Why Async/Await Inside forEach Is a Bad Idea
Did you ever run several asynchronous functions and pushed the results to an array, only to find out it's empty?
The values are there (you've console.log'ed them) and they're being pushed, how can the array be empty?!
So confusing.. 😕
Waiting for asynchronous functions to finish before running some other code seems like an impossible task.
But that's only because you haven't learned yet what tools to use.
When you learn how to combine Promise.all and .map into a powerful combo, you'll realize it's easier than you thought. 😎
Why async/await inside forEach doesn't do what you think it does
When you need to run an asynchronous operation for each element in an array, you might naturally reach out for the .forEach()
method.
For instance, you might have an array of user IDs and for each user ID, you want to fetch the user and push the username to an array. At the end, you log the usernames, but alas, the array is empty.
const userIds = [1, 2, 3];
const usernames = [];
userIds.forEach(async (userId) => {
const user = await fetchUserById(userId);
usernames.push(user.username);
});
// this prints an empty array, no usernames here 🙁
console.log(usernames);
So what's going on here? How does this code actually run? And how is that different from what you'd expect?
No better way to explain this than an animation of the JavaScript runtime as it executes the program line by line.
The first thing to notice is that the fetchUserById
requests are kicked off concurrently inside the forEach method. They're sent to background tasks until completion (WebAPIs in the browser, C++ thread pool in Node.js).
After all requests have started, the program then continues and logs the usernames
array, which of course is empty at this point because nothing has been pushed yet.
Aha!
You see, functional JavaScript methods like forEach are unaware of promises. Their callback functions are fired off synchronously without waiting for promises between each iteration.
Then after some time, the requests finish and the array is populated with the usernames. After which the program exits and the usernames are left behind in the dark. 👀
Seeing the runtime "jump up" from the end towards the middle is confusing because one of the first things we're taught in programming is that code executes from top to bottom.
So how do you run some code only after all asynchronous tasks have completed?
Promise.all
and .map
, a match made in heaven
The solution is to map each element of an array to a promise, and pass the resulting array of promises to Promise.all
. We have to use Promise.all because the await keyword only works on a single promise.
Promise.all is like an aggregator. Given an array of promises, it returns a single promise that fulfills after all promises have fulfilled. The returned promise resolves to an array with the results of the input promises.
To fix our example, we'll map the user IDs to an array of promises where each promise resolves with the username. We'll then pass the promises to Promise.all, await it and log the result.
const userIds = [1, 2, 3];
// Map each userId to a promise that eventually fulfills
// with the username
const promises = userIds.map(async (userId) => {
const user = await fetchUserById(userId);
return user.username;
});
// Wait for all promises to fulfill first, then log
// the usernames
const usernames = await Promise.all(promises);
console.log(usernames);
I've assigned the promises to a separate variable before passing them to Promise.all so it's easier to understand what's happening. In practice, you'll often see the map function written directly inside Promise.all.
That's it! You've learned how to run multiple asynchronous tasks and wait until they've completed before executing the remaining code. 👏
Next time you see async/await inside forEach it should raise an alarm bell because the code probably doesn't work as expected. ⚠️