先日、API Gateway + Lambda な環境で JavaScript のコード内で Array#forEach
に渡した async function
が実行されずにハマってたので書いておきます。
TL;DR
-
Array#forEach
にasync function
は渡さない方がいい -
Promise.all
とArray#map
を組み合わせると良さげ
現象
for
文は冗長な気がして、Array#forEach
や Array#map
を使いたい今日この頃。
配列に対して返り値を必要としない処理を行う場合、Array#forEach
を利用したくなりますが、 async function
を渡した場合にうまくいきません。
例えば、以下のコードを実行します。
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const asyncFunc = async (sec: number, idx: number) => {
await sleep(sec * 1000);
console.log(`sleep[${idx}]: ${sec} sec`);
};
const a = [4, 3, 2, 1];
a.forEach(asyncFunc);
console.log('finish');
$ ts-node foreach0.ts
finish
sleep[3]: 1sec
sleep[2]: 2sec
sleep[1]: 3sec
sleep[0]: 4sec
forEach
内の処理が終わる前に、finish が表示されています。
これは forEach
が返り値を全く考慮しない、たとえ Promise
が帰って来ようが処理を進めてしまうのが原因です。
対策
対策方法は、処理を順番に行いたい場合と、並列に行いたい場合で異なります。
- 順番に行いたい場合
配列の要素を順番に処理したい場合は for
文を使いましょう。
type AsyncFunc<T> = (x: T, i: number) => Promise<void>;
const asyncForEach = async <T>(ary: T[], fn: AsyncFunc<T>) => {
for (let i = 0; i < ary.length; i = i + 1) {
await fn(ary[i], i);
}
};
const main = async () => {
const a = [4, 3, 2, 1];
await asyncForEach(a, asyncFunc);
console.log('finish');
};
main();
$ ts-node foreach1.ts
sleep[0]: 4 sec
sleep[1]: 3 sec
sleep[2]: 2 sec
sleep[3]: 1 sec
finish
配列の順番に処理が実行されています。
インデックスが不要な場合は for..of
文を利用してもいいでしょう。
- 並列に行いたい場合
配列の順番に関係なく、並列に処理したい場合は Promise.all
と Array#map
を組み合わせます。
const parallelForEach = <T>(ary: T[], fn: AsyncFunc<T>) => (
Promise.all(ary.map((x, i) => fn(x, i)))
);
const main = async () => {
const a = [4, 3, 2, 1];
await parallelForEach(a, asyncFunc);
console.log('finish');
};
main();
$ ts-node foreach2.ts
sleep[3]: 1 sec
sleep[2]: 2 sec
sleep[1]: 3 sec
sleep[0]: 4 sec
finish
並列に処理が実行されています。
まとめ
-
Array#forEach
はasync function
を待ってくれない - 順番に処理したい場合は
for
文を使う - 並列に処理したい場合は
Promise.all
とArray#map
を組み合わせて使う
ちなみにハマった件では Promise.all
と Array#map
の方で何とかしました。