直列処理 ── 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 します。複数のエンドポイントから最速の成功を取りたいときに使います。
使い分けの判断基準
| パターン | 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 軸で選択が決まります。
- 直列・並行のどちらにするかは「速さ」だけでなく、「正しさ」の問題でもあります。依存がある処理を並行にすると結果が壊れます。