JavaScript | 非同期処理:Promise 基礎 – チェーン処理

JavaScript JavaScript
スポンサーリンク

まず「チェーン処理」を一言でいうと

Promise のチェーン処理は、
「非同期のステップを、then を使って“上から順番に”つなげていく書き方」 です。

コールバック地獄だと、

A の中で B を呼んで
B の中で C を呼んで
C の中で D を呼んで…

と、どんどん右にネストしていました。

Promise のチェーンはこれを、

A
→ then で B
→ then で C
→ then で D

という「縦にまっすぐな流れ」に変えてくれます。

ここが重要です。
チェーン処理の本質は、「時間の順番(A→B→C)を、ネストではなく縦の列として書けるようにすること」 です。


チェーン処理のいちばん基本の形

then が「新しい Promise を返す」という性質

Promise のチェーンを理解するうえで、一番大事なポイントはこれです。

then は必ず「新しい Promise」を返す。

これのおかげで、

「ある Promise の結果を受けて次の Promise を返す」
→ さらにその結果を受けて次…

というふうに「バトン渡し」ができます。

まずはとてもシンプルな例から見てみましょう。

Promise.resolve(1)
  .then((v1) => {
    console.log("v1:", v1);   // 1
    return v1 + 1;
  })
  .then((v2) => {
    console.log("v2:", v2);   // 2
    return v2 + 1;
  })
  .then((v3) => {
    console.log("v3:", v3);   // 3
  });
JavaScript

ここでは、

最初の Promise:1 で成功
1つ目の then:1 を受け取って 2 を返す
2つ目の then:2 を受け取って 3 を返す
3つ目の then:3 を受け取ってログを出す

というふうに、値が「上から下へ」流れていきます。

このとき、各 then の return は、

「次の then が受け取る値になる」

と覚えておくと良いです。

「普通の値」を返しても、「Promise」を返してもチェーンできる

then の中で、

単なる値(数値・文字列・オブジェクトなど)
を return した場合は、「その値で成功した Promise」が次に渡されます。

別の Promise
を return した場合は、「その Promise の結果」が次に渡されます。

この「どっちでもいい」という性質が、非同期処理をきれいにつなぐ鍵になります。


非同期処理をチェーンする実践例

擬似 API を 3 段つなげてみる

次のような「なんちゃって API 関数」を考えます。

ユーザーを取得する fetchUser
投稿を取得する fetchPosts(userId)
コメントを取得する fetchComments(postId)

いずれも Promise を返すものとします。

function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("ユーザー取得");
      resolve({ id: 1, name: "Taro" });
    }, 500);
  });
}

function fetchPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("投稿取得 userId:", userId);
      resolve([{ id: 10, title: "最初の投稿" }]);
    }, 500);
  });
}

function fetchComments(postId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("コメント取得 postId:", postId);
      resolve(["いいね", "すごい"]);
    }, 500);
  });
}
JavaScript

これをチェーン処理でつなげてみます。

fetchUser()
  .then((user) => {
    console.log("then1 user:", user);
    return fetchPosts(user.id);               // Promise を return
  })
  .then((posts) => {
    console.log("then2 posts:", posts);
    return fetchComments(posts[0].id);       // Promise を return
  })
  .then((comments) => {
    console.log("then3 comments:", comments);
  })
  .catch((err) => {
    console.error("どこかで失敗:", err);
  });
JavaScript

流れを丁寧に言葉で追うとこうです。

fetchUser() がユーザー情報で成功する
→ その結果 user を 1つ目の then が受け取る
→ そこで fetchPosts(user.id)(Promise)を return
→ チェーンはその Promise が終わるのを待つ

その Promise(fetchPosts)が成功すると、
→ 2つ目の then が posts を受け取る
→ そこで fetchComments(posts[0].id)(Promise)を return
→ またその Promise が終わるのを待つ

最後に fetchComments が成功すると、
→ 3つ目の then が comments を受け取ってログを出す

ここが重要です。
「return した Promise の完了を、次の then が自動的に待ってくれる」
これがチェーン処理の核心です。
自分で「コールバックの中で次の非同期処理を呼んで…」とネストを書く必要がなくなります。


コールバック地獄との違いを肌で感じる

ネスト版とチェーン版を見比べてみる

さきほどの処理を「コールバック地獄風」に書くとこうなります(イメージ)。

fetchUserCallback((err, user) => {
  if (err) { /* エラー処理 */ return; }

  fetchPostsCallback(user.id, (err, posts) => {
    if (err) { /* エラー処理 */ return; }

    fetchCommentsCallback(posts[0].id, (err, comments) => {
      if (err) { /* エラー処理 */ return; }

      console.log("全部そろった:", user, posts, comments);
    });
  });
});
JavaScript

横にずれていく「ピラミッド」になっているのが分かります。

一方、Promise のチェーン版ではこうでした。

fetchUser()
  .then((user) => fetchPosts(user.id))
  .then((posts) => fetchComments(posts[0].id))
  .then((comments) => {
    console.log("全部そろった:", comments);
  })
  .catch((err) => {
    console.error("どこかで失敗:", err);
  });
JavaScript

こちらは縦にスッと読めます。

ユーザーを取得
→ 投稿を取得
→ コメントを取得
→ 最後に全部そろった

というストーリーが、
コードの形とほぼ 1 対 1 で対応しています。

ここが重要です。
Promise チェーンは、「時間の順番」を「縦方向のコード」に変換する仕組み です。
これによって、頭の中でのシミュレーションがかなり楽になります。


チェーンの中で「値を変換する」パターン

非同期だけじゃなく、途中でデータを加工できる

then の中では、必ずしも次の非同期処理を返す必要はありません。
普通に値を加工して返すだけでも構いません。

fetchUser()
  .then((user) => {
    return {
      ...user,
      displayName: user.name + "さん",
    };
  })
  .then((userWithDisplayName) => {
    console.log(userWithDisplayName.displayName); // "Taroさん"
  });
JavaScript

ここでは、

1つ目の then:ユーザーを受け取って displayName を追加した新しいオブジェクトを返す
2つ目の then:その加工済みオブジェクトを受け取る

というふうに、「値を変換しながら流している」イメージです。

then の中で return しないとどうなるか

初心者がよくやるミスも触れておきます。

fetchUser()
  .then((user) => {
    console.log(user);
    fetchPosts(user.id); // これ、return していない
  })
  .then((posts) => {
    // ここでは posts は undefined になる
    console.log(posts);
  });
JavaScript

1つ目の then の中で fetchPosts(user.id) を呼んでいますが、
それを return していないため、

「何も返さない」=「undefined を返す」
→ 次の then には undefined が渡る

という動きになります。

「非同期処理をチェーンしたい」場合は、
「then の最後で必ず return する」
というクセをつけておくと安全です。


チェーンとエラーハンドリングの関係

どこか 1 箇所で catch すればよい

Promise チェーンの大きなメリットの一つは、
途中のどこで失敗しても、最後の catch でまとめて扱える ことです。

fetchUser()
  .then((user) => {
    return fetchPosts(user.id);
  })
  .then((posts) => {
    return fetchComments(posts[0].id);
  })
  .then((comments) => {
    console.log("成功:", comments);
  })
  .catch((err) => {
    console.error("どこかで失敗:", err);
  });
JavaScript

ここでは、

fetchUser が reject した
fetchPosts が reject した
fetchComments が reject した
どこかの then の中で throw した

どのパターンでも、最後の catch に流れてきます。

コールバック時代のように、

各階層で if (err) { ... } を書きまくる必要がなくなり、
コードの「正常系の流れ」に集中しやすくなります。

ここが重要です。
「正常系は then の縦の線で、異常系は最後の catch」という分離ができるのが、チェーンとエラーハンドリングの組み合わせの強み です。


初心者としての「チェーン処理」練習のすすめ方

シンプルなステップから自分で書いてみる

頭で分かるだけでなく、手で書いてみると一気に定着します。
例えばこんな流れの練習がおすすめです。

一つ目に、「数値を順番に加工する」チェーン。

1 から始めて
→ then で 2 倍
→ then で 3 足す
→ then でログに出す

など。

Promise.resolve(1)
  .then((v) => v * 2)
  .then((v) => v + 3)
  .then((v) => {
    console.log(v); // 5
  });
JavaScript

二つ目に、「setTimeout を使った疑似非同期」をつなげる。

function wait(ms, value) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`${ms}ms 後:`, value);
      resolve(value);
    }, ms);
  });
}

wait(500, "A")
  .then(() => wait(500, "B"))
  .then(() => wait(500, "C"))
  .then(() => {
    console.log("全部終わり");
  });
JavaScript

こういう小さなチェーンを何本か書いてみると、
「then で返したものが次に渡る」
という感覚が体に入ってきます。


まとめ:チェーン処理をどう捉えるか

Promise のチェーン処理を、一言で言い直すとこうなります。

「Promise が返す“結果のバトン”を、then を使って次々に渡していきながら、非同期処理をネストせずに縦に並べる書き方」 です。

押さえておいてほしいポイントは、

then は常に「新しい Promise」を返す
then の中で return した値や Promise が、「次の then」にそのまま渡る
これにより、「A のあと B、そのあと C」という時間の順番を、コード上では「縦のストーリー」として書ける
エラーは最後の catch でまとめて処理できるので、正常系のチェーンがとても読みやすくなる

まずは、

1→2→3 と数を変えていくシンプルなチェーン
setTimeout を使った簡単な擬似 API のチェーン

あたりから「自分の手」で書いてみてください。

その感覚がつかめたとき、
「Promise の一番おいしいところ(チェーンで非同期を直線化する)」が、
きっと腑に落ちているはずです。

タイトルとURLをコピーしました