In the accepted answer to a similar question the answer states that a forEach
call just throw a promise then exit. I think this should be the case as forEach
returns undefined
but why does the following code work?
const networkFunction = (callback) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(callback());
}, 200);
});
};
(async () => {
const numbers = [0, 1, 2];
// works in parallel
numbers.forEach(async (num) => {
await networkFunction(() => {
console.log("For Each Function: Hello");
});
});
})();
And it works in parallel this is the output of time node main.js # main.js contains only the mentioned code
❯ time node main.js
For Each Function: Hello
For Each Function: Hello
For Each Function: Hello
________________________________________________________
Executed in 365.63 millis fish external
usr time 126.02 millis 964.00 micros 125.05 millis
sys time 36.68 millis 618.00 micros 36.06 millis
The O.P.'s question was asking for clarification on another StackOverflow question found here. For further reading, and many other great answers on this general topic, please take a look at the link.
Don't use async/await with forEach. Either use a for-of
loop, or use Promise.all()
with array.map()
.
If you have a general understanding on promises and async/await, the TL;DR on the differences between promise.all()
+ array.map()
and .forEach()
, is that it's impossible to await a forEach()
itself. Yes, you can run tasks in parallel in .forEach()
just like you can with .map()
, but you can't wait for all of those parallel tasks to finish, then do something once they've all finished. The whole point of using .map()
instead of .forEach()
is so you can get a list of promises, collect them with Promise.all()
, then await the whole thing. To see what I mean, just put a console.log('Finished')
after a forEach(async () => ...)
, and you'll see the "finished"
get logged out before everything has finished running in the .forEach()
loop. My advice would be to just not use .forEach()
with async logic (and really there's not a reason use ever use .forEach()
anymore these days, as I explain further down).
For those who need something a bit more in depth, the rest of this answer will dive into more detail, first giving a small review on promises, then showing an in-depth explanation on how these approaches behave differently and why .forEach()
is always the inferior solution when it comes to async/await.
For the purposes of this discussion, you just have to remember that a promise is a special object that's promising that some task is going to be completed at some point in the future. You can attach listeners to a promise via .then()
, to get notified when the task is completed, and to receive the resolved value.
An async
function is simply a function that will always return a promise, no matter what. Even if you do async function doThing() { return 2 }
, it's not going to return 2, it's going to return a promise that immediately resolves to the value 2. Note that an asynchronous function will always return a promise immediately, even if it takes a long time for the function to run. This is why it's called a "promise", it's promising that the function will eventually finish running, and if you want to be notified for when the function has finished, you can add an event listener to it, via .then()
or await
.
await
is special syntax that lets you pause execution of an async function until a promise resolves. await
will only effect the function it's directly inside. Behind the scenes, await
is simply adding a special event listener to the promise's .then()
, so it can know when the promise resolves and what value it resolves with.
async function fn1() {
async function fn2() {
await myPromise; // This pauses execution of fn2(), not fn1()!
}
...
}
async function fn1() {
function fn2() {
await myPromise; // An error, because fn2() is not async.
}
...
}
If you can get a good grasp of these principles, then you should be able to understand the next sections.
for-of
A for-of
loop lets you execute your asynchronous tasks in serial, one after another. For example:
const delays = [1000, 1400, 1200];
// A function that will return a
// promise that resolves after the specified
// amount of time.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
async function main() {
console.log('start');
for (const delay of delays) {
await wait(delay);
console.log('Finished waiting for the delay ' + delay);
}
console.log('finish');
}
main();
The await
causes main()
to pause for the specified delay, after which the loop continues, the console.log()
executes, and the loop starts again with the next iteration, beginning a new delay.
This one should hopefully be a little straightforwards.
Promise.all()
+ array.map()
The use of Promise.all()
and array.map()
together lets you effectively run many asynchronous tasks in parallel, for example, we can wait for many different delays to finish at the same time.
const delays = [1000, 1400, 1200];
// A function that will return a
// promise that resolves after the specified
// amount of time.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
async function main() {
console.log('start');
await Promise.all(delays.map(async delay => {
await wait(delay);
console.log('Finished waiting for the delay ' + delay);
}))
console.log('finish');
}
main();
If you remember back to our quick primer on promises and async/await, you'll remember that await
only effects the function it's directly inside, causing that function to pause. In this case, the await
from await wait(delay)
will not be causing main()
to pause like it did in the previous example, instead, it's going to cause the callback being passed into delays.map()
to pause, because that's the function it's directly inside.
So, we have delays.map()
which will call the supplied callback, once for each delay
inside of the delays
array. The callback is asynchronous, so it's always going to return a promise immediately. The callback will begin executing with different delay arguments, but will inevitably hit the await wait(delay)
line, pausing execution of the callback.
Because the callback for .map()
returns a promise, delays.map()
is going to return an array of promises, which Promise.all()
will then receive, and will combine them together into one super promise that resolves when all of the promises in the array resolves. Now, we await
the super promise returned by promise.all()
. This await
is inside main()
, causing main()
to pause until all of the provided promises resolve. Thus, we've created a bunch of independent asynchronous tasks inside of main()
, let them all finish on their own time, and then paused the execution of main()
itself until all of these tasks have finished.
First of all, you really don't need to use forEach()
for anything. It came out before the for-of
loop did, and for-of
is simply better than forEach()
in every way. for-of
can run against any iterable, you can use break
and continue
with them, and, most importantly for this topic, await
will work as expected in for-of
but not in forEach()
. Here's why:
const delays = [1000, 1400, 1200];
// A function that will return a
// promise that resolves after the specified
// amount of time.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
async function main() {
console.log('start');
delays.forEach(async delay => {
await wait(delay);
console.log('Finished waiting for the delay ' + delay);
})
console.log('finish');
}
main();
First, you'll notice that .forEach()
will cause the tasks to run in parallel instead of serial, just like with .map()
. This is because the await
inside of forEach()
is only affecting the callback, not main()
. So, when we run delays.forEach()
, we call this asynchronous function for each delay
in delays
, kicking off a bunch of asynchronous tasks. The problem is that nothing will wait for the asynchronous tasks to finish, indeed, it's impossible to wait for them. The asynchronous callback is returning promises each time it gets called, but unlike .map()
, .forEach()
completely ignores the return value of its callback. .forEach()
receives the promise, and then simply ignores it. This makes it impossible to group the promises together and await
them all via Promise.all()
like we did previously. Because of this, you'll notice that "finish"
gets logged out immediately, since we're never causing main()
to wait for these promises to finish. This is likely not what you want.
(a.k.a. the original answer)
It works because the for loop still runs, and you still start a bunch of async tasks. The problem is that you're not awaiting them. Sure you use await within .forEach()
, but that only causes the .forEach()
callback to wait, it doesn't pause your outer function. You can see this if you put a console.log()
at the end of your async IIFE, your console.log() will fire immediately before all of your requests have finished. If you had used Promise.all() instead, then that console.log() would fire after the requests have finished.
Broken version:
const networkFunction = (callback) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(callback());
}, 200);
});
};
(async () => {
const numbers = [0, 1, 2];
// works in parallel
numbers.forEach(async (num) => {
await networkFunction(() => {
console.log("For Each Function: Hello");
});
});
console.log('All requests finished!');
})();
Fixed version:
const networkFunction = (callback) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(callback());
}, 200);
});
};
(async () => {
const numbers = [0, 1, 2];
// works in parallel
await Promise.all(numbers.map(async (num) => {
await networkFunction(() => {
console.log("For Each Function: Hello");
});
}));
console.log('All requests finished!');
})();