await をループの中で書くと1つずつ順番に待ってしまいます。Promise.all で同時に走らせる書き方、1つ失敗したときの扱い、forEach で待てない理由、同時数を絞る方法を確かめます。
答え
map() で先に全部始めてから、Promise.all() でまとめて待ちます。
example.js
const load = async (id) => id * 2;
console.log(await Promise.all([1, 2, 3].map(load)));
出力書き換えて実行できます
[ 2, 4, 6 ]
example.js
const load = async (id) => {
console.log('開始', id);
return id * 2;
};
for (const id of [1, 2, 3]) {
console.log('結果', await load(id));
}
出力書き換えて実行できます
開始 1
結果 2
開始 2
結果 4
開始 3
結果 6
結果は渡した順に並びます。 終わった順ではありません。
何が違うのか
for の中で await すると、1つ終わってから次を始めます。
example.js
const load = async (id) => {
console.log('開始', id);
return id * 2;
};
const running = [1, 2, 3].map(load);
console.log('結果', await Promise.all(running));
出力書き換えて実行できます
開始 1
開始 2
開始 3
結果 [ 2, 4, 6 ]
example.js
const ok = async (id) => id;
const ng = async () => { throw new Error('3 が読めません'); };
try {
await Promise.all([ok(1), ng(), ok(2)]);
} catch (error) {
console.log('落ちた:', error.message);
}
出力書き換えて実行できます
落ちた: 3 が読めません
「開始」と「結果」が交互に出ています。 前のが終わるまで次が始まっていません。
map() で先に全部始めると、こうなります。
example.js
const ok = async (id) => id;
const ng = async () => { throw new Error('失敗'); };
const results = await Promise.allSettled([ok(1), ng()]);
console.log(results);
console.log(results.filter((r) => r.status === 'fulfilled').map((r) => r.value));
console.log(results.filter((r) => r.status === 'rejected').map((r) => r.reason.message));
出力書き換えて実行できます
[ { status: 'fulfilled', value: 1 }, { status: 'rejected', reason: Error: 失敗 } ]
[ 1 ]
[ '失敗' ]
example.js
const ng = Promise.reject(new Error('だめ'));
const ok = Promise.resolve('よし');
console.log(await Promise.any([ng, ok]));
try {
console.log(await Promise.race([ng, ok]));
} catch (error) {
console.log('race は最初の決着:', error.message);
}
出力書き換えて実行できます
よし
race は最初の決着: だめ
「開始」が3つ続けて出ました。 3つとも走り出してから、まとめて待っています。
通信のように待ち時間が長い処理なら、かかる時間が全部の合計からいちばん遅い1つに縮みます。
計算だけの処理は速くなりません。JavaScript は1本の流れで動くので、待ち時間があるものにしか効きません。
1つ失敗すると全部落ちる
Promise.all() は、1つでも失敗した時点で全体が失敗になります。
javascript
const load = async (id) => id * 2;
const out = [];
[1, 2, 3].forEach(async (id) => {
out.push(await load(id));
});
console.log(out);
[]
example.js
const load = async (id) => id * 2;
console.log(await Promise.all([1, 2, 3].map(load)));
出力書き換えて実行できます
[ 2, 4, 6 ]
成功した分も受け取れません。 全部そろわないと意味がないとき(画面を描くのに3つとも要る、など)は、これが正しい動きです。
失敗しても他を使いたいなら
Promise.allSettled() は、全部の結果を成否つきで返します。
example.js
const load = async (n) => n * 10;
const ids = [1, 2, 3, 4, 5];
const size = 2;
const out = [];
for (let i = 0; i < ids.length; i += size) {
const chunk = ids.slice(i, i + size);
console.log('この回:', chunk);
out.push(...await Promise.all(chunk.map(load)));
}
console.log(out);
出力書き換えて実行できます
この回: [ 1, 2 ]
この回: [ 3, 4 ]
この回: [ 5 ]
[ 10, 20, 30, 40, 50 ]
例外は投げられません。 status を見て自分で分けます。
| 使うもの |
動き |
Promise.all() |
1つ失敗したら全体が失敗。全部そろわないと困るとき |
Promise.allSettled() |
全部待って成否を返す。取れた分だけ使いたいとき |
Promise.any() |
1つ成功したらそれを返す |
Promise.race() |
成否を問わず最初に決着したものを返す |
any() は失敗を飛ばして成功を待ちます。
race() は失敗でも最初なら返します。取り違えると、失敗が握りつぶされます。
よくある間違い
forEach の中で await する
空のままです。
forEach() はコールバック関数の戻り値を見ません。
async を付けても Promise が返るだけで、forEach() はそれを捨てて次へ進みます。
待っているつもりで、待っていません。
map() に変えて Promise.all() で待てば直ります。
forEach は非同期処理と組み合わせない、と覚えてしまうのが安全です。
全部いっぺんに投げる
要素が1000件あれば、Promise.all() は1000本を同時に走らせます。
相手のサーバーに断られたり、こちらの資源が尽きたりします。
数を区切ってください。
塊の中は並列、塊同士は直列になります。
size で同時に走る数を決められます。
最後の塊は端数(1件)になります。slice() は範囲を超えても落ちないので、割り切れなくても書き足す必要はありません。
→ 文字列を切り出す・整える
まとめ
map() で先に始めて Promise.all() で待つ。結果は渡した順
- 待ち時間のある処理にだけ効く。計算は速くならない
Promise.all() は1つ失敗すると全部失敗。成功分も受け取れない
- 取れた分だけ使うなら
Promise.allSettled()
forEach の中の await は待たない。 map() に変える
- 件数が多いときは塊に区切って同時数を抑える
forEach() と map() の違いそのもの(戻り値を見るか捨てるか)は、用語のページで詳しく扱っています。
→ Array.prototype.map()