JavaScript
TypeScript

forEach と async function

先日、API Gateway + Lambda な環境で JavaScript のコード内で Array#forEach に渡した async function が実行されずにハマってたので書いておきます。


TL;DR



  • Array#forEachasync function は渡さない方がいい


  • Promise.allArray#map を組み合わせると良さげ


現象

for 文は冗長な気がして、Array#forEachArray#map を使いたい今日この頃。

配列に対して返り値を必要としない処理を行う場合、Array#forEach を利用したくなりますが、 async function を渡した場合にうまくいきません。

例えば、以下のコードを実行します。


foreach0.ts

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 が帰って来ようが処理を進めてしまうのが原因です。


対策

対策方法は、処理を順番に行いたい場合と、並列に行いたい場合で異なります。


  1. 順番に行いたい場合

配列の要素を順番に処理したい場合は for 文を使いましょう。


foreach1.ts

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 文を利用してもいいでしょう。


  1. 並列に行いたい場合

配列の順番に関係なく、並列に処理したい場合は Promise.allArray#map を組み合わせます。


foreach2.ts

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#forEachasync function を待ってくれない

  • 順番に処理したい場合は for 文を使う

  • 並列に処理したい場合は Promise.allArray#map を組み合わせて使う

ちなみにハマった件では Promise.allArray#map の方で何とかしました。