直列処理 ── for...of + await で順序を保証する

直列処理とは、1 つの非同期処理が完了してから次を開始する実行パターンです。「前の結果を次のリクエストのパラメータに使いたい」「副作用を順番に確定させたい」といった依存関係がある場合に選びます。

const ids = [1, 2, 3];
const results = [];

for (const id of ids) {
  const data = await fetchUser(id);  // 1 → 2 → 3 の順に完了を待つ
  results.push(data);
}

for...of ループの中で await すると、各反復で処理の完了を待ってから次へ進みます。合計時間は各処理の時間の総和になります。独立した処理を直列にするのは無駄ですが、依存がある場合はこれが正しい選択です。

並行処理:Promise.all ── 全部投げて全部待つ

並行処理は、複数の非同期処理を同時に開始し、すべての完了を待つパターンです。ここで重要な用語の区別があります。

  • 並列(parallel)— 複数の処理が物理的に同時に動きます。マルチコアやマルチスレッドが前提です。
  • 並行(concurrent)— 複数の処理を論理的に「重ねて」進めます。シングルスレッドでも実現できます。

JavaScript はシングルスレッドなので、Promise.all が実現するのは「並行」です。ネットワークリクエストのような I/O 待ちはランタイムに委譲されるため、待ち時間を重ねることができます。

const [user, posts, comments] = await Promise.all([
  fetchUser(1),
  fetchPosts(1),
  fetchComments(1),
]);

3 つのリクエストをほぼ同時に開始し、最も遅いものが終わるまで待ちます。合計時間は総和でなく最大値に近づきます。ただし 1 つでも reject されると即座に全体が reject になる点に注意が必要です。

よくある罠:await してから Promise.all に渡すと直列になる

Promise.all を使っているつもりで、実は直列になっているコードはよく見かけます。

// 罠:これは直列になる
const user = await fetchUser(1);      // ここで待つ
const posts = await fetchPosts(1);    // ここで待つ

const result = await Promise.all([user, posts]);  // すでに解決済みの値を渡している

await を先に書くと、その時点で処理の完了を待機してしまいます。Promise.all に渡るのは Promise ではなく、すでに解決した値です。並行の恩恵はまったく得られません。

正しくは、Promise(未解決の非同期操作)を配列に入れて渡します

// 正解:Promise を渡す(await しない)
const result = await Promise.all([fetchUser(1), fetchPosts(1)]);

関数呼び出し fetchUser(1) はその時点で非同期処理を開始します。await せずに Promise.all へ渡せば、2 つの処理が並行して走ります。

エラーを許容したい:Promise.allSettled

Promise.all は 1 件の失敗で全体が reject になります。「失敗してもほかの結果は欲しい」場合には Promise.allSettled を使います。

const results = await Promise.allSettled([
  fetchUser(1),
  fetchPosts(999),  // 存在しない ID で失敗する想定
  fetchComments(1),
]);

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log(result.value);
  } else {
    console.error(result.reason);
  }
}

allSettled はすべての処理が完了(成功・失敗どちらでも)するまで待ち、{ status, value | reason } の配列を返します。部分的な成功を受け入れてエラーを個別に処理するバッチ処理に向いています。

競争させる:Promise.race / Promise.any

Promise.race は最初に settle(解決または拒否)した Promise の結果を返します。タイムアウト実装でよく使われます。

const result = await Promise.race([
  fetchData(),
  new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3000)),
]);

Promise.any は最初に fulfilled(成功)した Promise の結果を返します。すべて reject になった場合だけ AggregateError で reject します。複数のエンドポイントから最速の成功を取りたいときに使います。

Promise メソッドの比較

使い分けの判断基準

パターン API 使いどころ
直列 for...of + await 前の結果が次に必要。副作用を順番に確定させたい
全件並行・1 件失敗で終了 Promise.all 独立した処理を並行。全件成功が前提
全件並行・失敗も収集 Promise.allSettled 部分的な成功を受け入れるバッチ処理
最速のものを取る Promise.race タイムアウト、最初の応答優先
最初の成功を取る Promise.any フォールバック付きの並行リクエスト

まとめ

  • Promise.all が実現するのは並行であって並列ではありません。I/O 待ちを重ねることで合計時間を短縮します。
  • await してから Promise.all に渡すのは罠です。未解決の Promise(関数呼び出し結果)を直接渡します。
  • 1 件の失敗で全体を止めたくないなら Promise.allSettled を選びます。
  • 処理の依存関係(前の結果が次に必要か)とエラー戦略(1 件の失敗をどう扱うか)の 2 軸で選択が決まります。
  • 直列・並行のどちらにするかは「速さ」だけでなく、「正しさ」の問題でもあります。依存がある処理を並行にすると結果が壊れます。