await とループを一言でいうと
await をループの中で使うときのポイントは、
「そのループを“順番にゆっくり回す”のか、“できるだけ同時に走らせる”のかを意識すること です。
同じループでも、
- 各要素を「1 個ずつ順番に await する書き方」
- 全要素の処理を「まとめて並列に走らせてから await する書き方」
があり、これを意識せず書くと、
「無駄に遅いコード」や「思ったとおり動かないコード」になりがちです。
ここが重要です。await は「その行で一旦止まる」ので、
ループの中に置けば「ループを止める」ことにもなります。
止めたいのか、止めたくないのかを、自分で決めて書けるようになることが大切です。
await と相性が良いループ:for / for…of
1件ずつ順番に処理したいときの基本形
まず、「配列を順番に処理したい」典型パターンから。
async function processItemsSerial(items) {
for (const item of items) {
const result = await doAsync(item);
console.log("処理結果:", result);
}
console.log("全部終わり");
}
JavaScriptこのコードの動きはこうです。
- 最初の要素で
doAsync(item)を実行 awaitでその結果を待つ(終わるまで次のループには進まない)- 結果をログに出す
- 次の要素に進む
- これを配列の最後まで繰り返す
つまり、「1 件ずつ順番に処理する」直列処理 です。
順番に処理することに意味があるケース
例えば、
- API に「1秒に1回まで」という制限があるとき
- 「前の結果が次の処理に影響する」ようなとき
- 「順番どおりに処理していきたい」仕様のとき
などは、あえてこの「直列」のループが必要になります。
async function uploadInOrder(files) {
for (const file of files) {
const result = await uploadFile(file);
console.log("アップロード完了:", result);
}
}
JavaScriptここが重要です。
for / for…of の中で await を使うと、「配列の長さ × 非同期処理時間」のぶんだけ、きっちり時間がかかる。
それは“遅い”のではなく、“順番を守る”という意味で正しい挙動です。
“順番が必要かどうか”を自分で判断することが大事です。
よくある落とし穴:forEach と await
forEach の中で await は「待ってくれない」
初心者がよくハマるポイントがこれです。
async function processItems(items) {
items.forEach(async (item) => {
const result = await doAsync(item);
console.log("結果:", result);
});
console.log("forEach の外側");
}
JavaScriptパッと見、「順番に await してくれそう」に見えますが、
実際にはこうなります。
forEachはコールバックを「同期的に」全部呼び出してしまう- そのコールバック内の
awaitは、その関数の中で止まるだけ processItems関数自体は、forEachが終わった瞬間に次へ進む
そのため、「forEach の外側」のログは、
各 doAsync が終わる前に先に実行されてしまいます。
つまり、forEach は
「ループの外から見て await を効かせたい」用途には向いていません。
なぜ for / for…of をすすめるのか
for / for…of なら、
ループ自体が await によって“止まる”ので、
「全部終わってから次に進む」という直感どおりの動きになります。
async function processItems(items) {
for (const item of items) {
const result = await doAsync(item);
console.log("結果:", result);
}
console.log("全部終わったあとにこれが出る");
}
JavaScriptここが重要です。
「ループ全体として、await をちゃんと効かせたい」なら、forEach や map のコールバックに async を渡すのではなく、
素直に for / for...of で書いたほうが、挙動もコードも分かりやすくなります。
配列に対する「並列処理」:map + Promise.all
全要素を同時に処理したい場合
今度は逆に、こういう状況を考えます。
- 各アイテムの処理は「互いに独立」
- 1 件ずつ順番にやる必要はない
- できるだけ早く全部終わらせたい
こういうときは、「配列を map して Promise の配列を作り、Promise.all でまとめて await」するのが定番です。
async function processItemsParallel(items) {
const promises = items.map((item) => {
return doAsync(item); // doAsync は Promise を返す
});
const results = await Promise.all(promises);
console.log("全部の結果:", results);
}
JavaScript動きはこうです。
items.map(...)が、全アイテムに対してdoAsync(item)を一気に呼び出す
→ 各処理が“ほぼ同時に”スタートpromisesは、「各処理の Promise が入った配列」になるPromise.all(promises)で、「全部終わるまで待つ Promise」を作るawaitでその完了を待ち、resultsに全結果が配列で入る
もし doAsync が 1 件あたり 1 秒かかる処理でも、
配列が 10 件だとして、
直列なら約 10 秒、並列なら約 1 秒ちょっとで終わるイメージです。
map + Promise.all のイディオム
まとめると、
「配列を全部並列で処理して、結果を全部欲しい」パターンは以下の形を覚えてしまって構いません。
const results = await Promise.all(items.map(item => doAsync(item)));
JavaScriptここが重要です。
「順番に処理する for + await」と
「並列で処理する map + Promise.all + await」を、
意識的に使い分けられるかどうかが、
“await とループを理解できているか” の一つの分かれ目です。
直列 vs 並列:どちらを選ぶかの判断基準
直列 for + await が向いているケース
例えば次のような条件なら、「順番に await」するべきです。
- 「前の処理が終わらないと、次の処理の内容が決められない」
例:userIdを取り、そのuserIdで投稿を取り、その投稿 ID でコメントを取る - API の利用制限を守りたい(リクエストを間引きたい)
- ファイルを 1 件ずつ順番に上書きしたいなど、順序性が重要な処理
並列 map + Promise.all が向いているケース
こちらは「順番にやる意味はない」「できるだけ早く終わってほしい」ケース。
- 商品 ID のリストに対して、在庫情報をそれぞれ取得
- ユーザーの一覧に対して、プロフィール画像をそれぞれダウンロード
- いくつかの外部 API を同時に叩いて、全部揃ったら画面を描画
要は、「全部終われば良い。誰が先でも構わない」 場面です。
ここが重要です。
ループの中で await を見つけたら、
「これは本当に直列にする必要があるか?」
「個々が独立しているなら、Promise.all にできないか?」
と一度問い直す習慣を持つと、
非同期処理のパフォーマンスと設計力が一気に上がります。
ちょっと応用:for…of で“制限付き並列”をする考え方
「全部いっぺんに」はヤバいこともある
Promise.all で一気に並列にすると、
例えば 1000 件のリクエストを同時に飛ばしてしまう可能性があります。
それは、
- サーバー側に負荷をかけすぎる
- ブラウザや Node.js のコネクション数上限に引っかかる
などの問題につながります。
「いくつかずつ並列に処理する」発想
ここは少し応用になりますが、
for…of と await で、「同時に走らせる数を制限する」という書き方もできます。
例えば「最大 3 件まで並列にしたい」など。
初学者の段階でここまで厳密に書けなくても構いませんが、
「直列か全部並列か」の二択ではなく、
「並列度合いを調整する」という考え方がある、ということだけ頭の片隅に置いておくとよいです。
ここが重要です。
await とループは、「完全直列」と「完全並列」だけではなく、
“どのくらい並列にするか” を調整する道具にもなりえます。
今はざっくりでいいので、「並列度」という観点があると知っておいてください。
初心者として「await とループ」で本当に押さえてほしいこと
for / for…of の中で await を使うと、「1 件ずつ順番に処理する」直列ループになる。
これは「遅い」のではなく、「順序を守っている」と理解する。
forEach のコールバックで async / await を使っても、
ループ全体としては待ってくれない。
「ループの外側からちゃんと待ちたい」ときは、for / for…of を使う。
「配列の要素を全部“同時に”処理して結果だけ欲しい」ときは、await Promise.all(items.map(item => doAsync(item))) という形が定番。
直列ループにするか並列処理にするかは、
「前の結果に依存しているか」「順番が意味を持つか」を基準に判断する。
ここが重要です。
await とループを使いこなす鍵は、「時間」と「依存関係」を意識することです。
“本当にここで止まりたいのか?”
“ここはみんなで一斉に走り出してもいい場所じゃないか?”
と自分に問いかけながら await の場所を決めていくと、
非同期処理が“ただ動くだけ”から“自分で設計した動き”に変わっていきます。
最後に、小さな練習を置いておきます。
// 1. 1秒後に値を返す doAsync(i) を作る(i をそのまま返してもよい)。
// 2. 配列 [1,2,3,4,5] を「直列 for...of + await」で処理して、
// 毎回のログと全体の時間を測ってみる。
// 3. 同じ処理を「map + Promise.all + await」で書いて、
// ログと時間の違いを比べてみる。
JavaScript自分の目で「どこで止まっているか」「どれだけ待ち時間が変わるか」を確かめると、
await とループの関係が、かなり立体的に見えてくるはずです。
