How to use forEach with async/await
A synchronous for loop works similarly to a forEach
for blocking operations.
These two do the same:
const arr = [100, 70, 50];
for(let i of arr) {
console.log(i); // 100, 70, 50
}
arr.forEach((i) => {
console.log(i); // 100, 70, 50
})
But as soon as async comes into play, this behavior changes:
for(let i of arr) {
await wait(i);
console.log(i);
}
console.log("done");
// 100, ~100 ms later
// 70, ~70 ms later
// 50, ~50 ms later
// done, after that
But with forEach
, it works differently:
arr.forEach((i) => {
wait(i);
console.log(i);
})
console.log("done");
// 100, instantly
// 70, instantly
// 50, instantly
// done, instantly
While the former waits the appropriate amount of time between printing the numbers, the latter does not wait at all.
Let's await
Oh wait, there is no await
for the wait
!
Let's fix this:
arr.forEach(async (i) => {
await wait(i);
console.log(i);
})
console.log("done");
// done, instantly
// 50, ~50 ms later
// 70, ~20 ms later
// 100, ~30 ms later
It does not wait for completion, which is still not the same as with the for..of
loop.
Wait for completion
To fix this, as it turns out, we need to change the forEach
to a map
, and wait for all the Promises using Promise.all
:
await Promise.all(arr.map(async (i) => {
await wait(i);
console.log(i);
}))
console.log("done");
// 50, ~50 ms later
// 70, ~20 ms later
// 100, ~30 ms later
// done, after that
Run sequentially
One last difference is that the elements are run simultaneously. To change it to sequential execution, even the Promise.all
+ map
combo falls short, and we need to use a different structure.
This solution uses a reduce
:
await arr.reduce(async (prev, i) => {
await prev;
await wait(i);
console.log(i);
}, Promise.resolve());
console.log("done");
// 100, ~100 ms later
// 70, ~70 ms later
// 50, ~50 ms later
// done, after that
The code above faithfully emulates the for..of
loop we started with.