概要
非同期ループとは「ループの中でawaitを使うこと」ではない。
それは**“逐次処理と並列処理を文脈に応じて正しく使い分け、意図的に制御するためのループ設計戦略”**である。
JavaScriptでは forEach
の中で await
を使っても期待通りに動作しない。
多くの開発者がこの罠にはまり、**「非同期が効いていない」「順序通りに実行されない」**といった問題に直面する。
本稿では、非同期処理とループ構造の組み合わせに潜む罠と、安全かつ意図的な設計戦略を整理する。
1. forEach
で await
が機能しない理由
[1, 2, 3].forEach(async (n) => {
await doSomething(n);
console.log(n);
});
- ❌
forEach
はawait
を“待たない” - ❌ 結果として 非同期処理の完了前にループが終了
- ❌
forEach
はPromise
を制御できない →async
の外側と非同期の整合が取れない
2. 正しい選択肢:for...of + await
for (const n of [1, 2, 3]) {
await doSomething(n);
console.log(n);
}
- ✅ 逐次処理を明示的に保証
- ✅ 非同期の完了を1つずつ待ちつつ処理できる
3. 並列処理が必要なケース:Promise.all
の活用
await Promise.all([1, 2, 3].map(async (n) => {
await doSomething(n);
}));
- ✅ 全要素を同時に非同期実行
- ✅ 順序不要な処理 / 高速化が必要な場面に有効
4. 順序を保証しつつ並列性を担保するパターン
for (const n of [1, 2, 3]) {
queue.push(async () => {
await doSomething(n);
});
}
// 実行
for (const task of queue) {
await task();
}
- ✅ 逐次化されたPromiseのキューを設計上で明示的に制御
- ✅ イベントキューやフロー制御の設計にも応用可能
5. map
/ filter
などとの併用にも注意
const results = await Promise.all(items.map(async item => {
const data = await fetchData(item);
return process(data);
}));
- ✅
.map()
+Promise.all()
は 非同期mapとしての基本パターン - ❌
.filter()
や.reduce()
は非同期に非対応 → 逐次化が必要
設計判断フロー
① 非同期処理は「順序が重要」か「並列でOK」か?
② awaitをforEachの中に書いていないか?(待たれない)
③ map/filterで非同期を使う場合、Promise.allで制御しているか?
④ UIやAPIが逐次処理を要求していないか?(レート制限など)
⑤ 逐次 + 非同期を組み合わせる必要があるなら for...of を使用しているか?
よくあるミスと対策
❌ forEachでawaitしても順番に実行されない
→ ✅ for...ofに書き換えて処理を逐次化する
❌ Promise.allで非同期処理を飛ばして順序が崩れる
→ ✅ 順序が重要な処理は明示的なawaitループに変換
❌ async関数をfilterやreduceで使ってエラー or 謎の動作
→ ✅ filterやreduceにはasync対応の別ロジックを設計する(ループ or reduceAsync)
結語
非同期ループ設計とは「awaitを中に書くだけ」ではない。
それは**“処理の順序性と並列性、そして文脈に応じた制御構造を設計として明示すること”**である。
- 順序が必要なら
for...of
- 並列でよければ
Promise.all
-
forEach
/filter
/reduce
にawait
を使うのは原則非推奨
JavaScriptにおける非同期ループとは、
“処理の意図と制御の文脈を設計によって正確に伝える構造戦略”である。