JavaScript | 非同期処理:Promise 応用 – 逐次処理の書き方

JavaScript JavaScript
スポンサーリンク

まず「逐次処理」とは何かをはっきりさせる

Promise で言う「逐次処理(ちくじ処理)」は、
「非同期処理を A → B → C… のように“順番に”実行する書き方」 のことです。

具体的には、

A が終わってからでないと B を始められない
B の結果を使って C をしたい

といったように、前の結果に次の処理が依存している 場面で必要になります。

ここが重要です。
Promise を使うときの基本は、

  • 「依存関係があるところ」は 逐次処理(順番に)
  • 「依存関係がないところ」は 並列処理(同時に)

というふうに書き分けることです。
ここでは、その「順番に書く側」=逐次処理の書き方に絞って、基礎から丁寧に整理していきます。


コールバック地獄との違いから見る逐次処理

コールバックで逐次処理を書くとどうなるか

例えば、

  1. ユーザー情報を取得
  2. そのユーザーの投稿一覧を取得
  3. その中の 1 件目の投稿のコメントを取得

という3ステップを考えます。

コールバックだけで書くと、だいたいこんな感じになります。

getUser((err, user) => {
  if (err) {
    console.error("ユーザー取得失敗:", err);
    return;
  }

  getPosts(user.id, (err, posts) => {
    if (err) {
      console.error("投稿取得失敗:", err);
      return;
    }

    getComments(posts[0].id, (err, comments) => {
      if (err) {
        console.error("コメント取得失敗:", err);
        return;
      }

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

やっていること自体は「逐次処理」ですが、

インデントがどんどん深くなる
if (err) が何度も出てくる
どこで何をしているのかパッと見て分かりづらい

という問題が出ます。いわゆる「コールバック地獄」です。

Promise で同じ逐次処理を書くとどうなるか

同じステップを Promise ベースで書くと、こうなります。

getUserPromise()
  .then((user) => {
    return getPostsPromise(user.id);
  })
  .then((posts) => {
    return getCommentsPromise(posts[0].id);
  })
  .then((comments) => {
    console.log("コメント:", comments);
  })
  .catch((err) => {
    console.error("どこかで失敗:", err);
  });
JavaScript

インデントは深くならず、
「上から下に」処理の順番が読める形 になっています。

ここが重要です。
逐次処理そのものはコールバックのときも Promise のときも同じですが、
Promise を使うと 「順番」と「エラーハンドリング」をきれいに表現できる ようになります。


then チェーンで書く基本的な逐次処理

一番素直な「A → B → C」の形

まずは、値だけで考えてみます。

Promise.resolve(1)
  .then((v1) => {
    console.log("v1:", v1);        // 1
    return v1 + 1;                 // 2 を次に渡す
  })
  .then((v2) => {
    console.log("v2:", v2);        // 2
    return v2 * 3;                 // 6 を次に渡す
  })
  .then((v3) => {
    console.log("v3:", v3);        // 6
  })
  .catch((err) => {
    console.error("エラー:", err);
  });
JavaScript

ここでのポイントを整理すると、

最初の Promise が 1 で成功
1つ目の then が v1 = 1 を受け取り、return 2
→ 2 が次の Promise の成功値になって、2つ目の then に v2 = 2 として渡る
2つ目の then が return 6
→ 6 が次の then に v3 = 6 として渡る

という「値のバトンリレー」が起きています。

then の中で return したものが、次の then の入力になる
これが逐次処理の基本ルールです。

非同期処理を順番につなげる

次に、実際の非同期処理を順番につなげる例です。

function step1(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("step1 完了");
      resolve(x + 1);
    }, 500);
  });
}

function step2(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("step2 完了");
      resolve(x * 2);
    }, 500);
  });
}

function step3(x) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("step3 完了");
      resolve(x - 3);
    }, 500);
  });
}

Promise.resolve(1)
  .then((v) => step1(v))
  .then((v) => step2(v))
  .then((v) => step3(v))
  .then((final) => {
    console.log("最終結果:", final);
  })
  .catch((err) => {
    console.error("どこかで失敗:", err);
  });
JavaScript

時間の流れは、

最初の値 1
→ 0.5秒待って step1 → 2
→ 0.5秒待って step2 → 4
→ 0.5秒待って step3 → 1

と、「順番に」実行されています。

ここが重要です。
逐次処理では、「次の then が動くのは、前の Promise が終わった“あと”」
Promise が返る関数を順に return していくことで、時間の順番をきれいに表現できます。


return を忘れたときの典型的なバグ

よくある悪い書き方

初心者が逐次処理でよくハマるパターンが「return し忘れ」です。

fetchUser()
  .then((user) => {
    console.log("user:", user);
    fetchPosts(user.id);        // ← return していない
  })
  .then((posts) => {
    console.log("posts:", posts); // ここでは posts は undefined になる
  })
  .catch((err) => {
    console.error("エラー:", err);
  });
JavaScript

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

この場合、

1つ目の then の戻り値は undefined
→ 次の then は「undefined で成功した Promise」として動き出す
→ 2つ目の then の posts には何も入っていない

という状態になります。

正しい書き方

fetchUser()
  .then((user) => {
    console.log("user:", user);
    return fetchPosts(user.id);   // Promise を return する
  })
  .then((posts) => {
    console.log("posts:", posts); // ここで投稿を使える
  })
  .catch((err) => {
    console.error("エラー:", err);
  });
JavaScript

ここが重要です。
逐次処理で「次のステップの Promise」を呼ぶときは、「必ず return する」。
return しないとチェーンが切れて、値も順番も崩れる。

「then の最後は return で終わる」くらいのクセをつけておくと安全です。


async/await と比較してみる(考え方の整理)

then チェーン版と async/await 版

同じ逐次処理でも、async/await を使うともっと「同期っぽく」書けます。

さきほどの

  • ユーザーを取得
  • 投稿を取得
  • コメントを取得

を then チェーンと async/await で比べてみます。

then チェーン版:

getUserPromise()
  .then((user) => {
    return getPostsPromise(user.id);
  })
  .then((posts) => {
    return getCommentsPromise(posts[0].id);
  })
  .then((comments) => {
    console.log("コメント:", comments);
  })
  .catch((err) => {
    console.error("どこかで失敗:", err);
  });
JavaScript

async/await 版:

async function main() {
  try {
    const user = await getUserPromise();
    const posts = await getPostsPromise(user.id);
    const comments = await getCommentsPromise(posts[0].id);
    console.log("コメント:", comments);
  } catch (err) {
    console.error("どこかで失敗:", err);
  }
}

main();
JavaScript

やっていること(逐次処理)自体は全く同じです。

async/await は「Promise ベースの逐次処理を、同期っぽい文法で書きやすくしたもの」なので、
まず then チェーンで逐次処理の考え方を理解してから async/await に行く と、理解がかなりスムーズになります。

ここが重要です。
根っこにあるのは always Promise。
then チェーンは Promise を「直線に並べてつなぐ」、
async/await はその見た目を少し変えているだけ。


配列データを「順番に」処理したいときの逐次処理パターン

forEach では逐次にならない

例えば、「配列に入っている URL を、一つずつ順番にリクエストしたい」とします。

やってしまいがちな書き方がこれです。

urls.forEach((url) => {
  fetch(url).then((res) => {
    console.log("完了:", url);
  });
});
JavaScript

これは「全部同時にスタート」しているので、並列処理 です。
一つ一つが順番に終わる保証はありません。

「1つ目が終わってから 2 つ目」「2つ目が終わってから 3 つ目」という逐次にはなりません。

reduce を使った Promise チェーンで逐次処理にする

Promise で「配列を逐次処理」するときによく使われるパターンが、reduce です。

function fetchInSequence(urls) {
  return urls.reduce((prevPromise, url) => {
    return prevPromise.then(() => {
      return fetch(url).then((res) => {
        console.log("完了:", url);
        return res;
      });
    });
  }, Promise.resolve());
}

const urls = ["/a", "/b", "/c"];

fetchInSequence(urls).then(() => {
  console.log("全部順番に終わった");
});
JavaScript

流れを説明すると、

最初の prevPromise は Promise.resolve()(すぐ成功する Promise)
1つ目の URL で「前が終わったら fetch(url1)」を then でつなぐ
2つ目の URL で「前が終わったら fetch(url2)」をさらに then でつなぐ
3つ目も同様

というふうに、
「前の Promise が終わってから次の fetch」が必ず呼ばれるチェーン を作っていっています。

結果として、

fetch(url1) → 完了
→ fetch(url2) → 完了
→ fetch(url3) → 完了

というきれいな逐次処理になります。

いきなり reduce は難しければ、まずは async/await で書いても構いません。
ただ、「Promise だけでも配列の逐次処理は書けるんだ」と知っておくと、Promise 自体への理解がぐっと深まります。


初心者としての「逐次処理の押さえどころ」

最後に、Promise で逐次処理を書くうえで、本当に大事なポイントだけを整理します。

逐次処理とは、「前の非同期処理が終わってから次をやる」こと。
前の結果に次が依存している場面で必須。

then チェーンでの基本形は「A の then で B を return」「B の then で C を return」…という形。
then の return が、次の then の引数になる(値のバトンリレー)。

非同期関数(Promise を返す関数)を順番につなぐときは、「then の中で必ず return する」。
return を忘れるとチェーンが切れて、逐次にならなくなる。

配列を逐次処理したいなら、forEach ではなく、Promise チェーン(reduce)か async/await+for ループを使う。

ここが重要です。
Promise での逐次処理は、「then を縦に並べて、各 then の最後で次の Promise を return する」というシンプルなルールさえ守れれば怖くありません。

もしよければ、

setTimeout を使って

  • 1秒後に「step1」
  • 1秒後に「step2」
  • 1秒後に「step3」

と順番に表示される逐次処理を、
then チェーンだけで一度書いてみてください。

自分の手で書いて「確かに順番に実行されている」と確認できたとき、
Promise の逐次処理が「ただの決まりごと」でしかないことが、きっと感覚的に分かってきます。

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