Skip to content

Why Async/Await Inside forEach Is a Bad Idea

4 min read

Did you ever run several asynchronous functions and pushed the results to an array, only to find out it's empty?

A broken code example where forEach is used on an array of urls to fetch then push the responses to an array. Right after the forEach the array is logged which shows up as 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.

A visual representation of how Promise.all works. A cone that takes in an array of promises and unifies them to a single promise that resolves with the results of 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. ⚠️

Transform Callbacks into Clean Async Code! 🚀

Tired of messy callback code? Download this FREE 5-step guide to master async/await and simplify your asynchronous code.

In just a few steps, you'll transform complex logic into readable, modern JavaScript that's easy to maintain. With clear visuals, each step breaks down the process so you can follow along effortlessly.

Refactoring Callbacks Guide Preview

You'll also get tips on building scalable Node.js applications about twice a month. I respect your email privacy. Unsubscribe any time.

You might also like

Run Concurrent Tasks With a Limit Using Pure JavaScript

Run asynchronous tasks with a concurrency limit without having to rely on an external library.
Read article

14 Linting Rules To Help You Write Asynchronous Code in JavaScript

A compiled list of linting rules to specifically help you with writing asynchronous code in JavaScript and Node.js.
Read article

Avoid This Mistake When Caching Asynchronous Results

Caching is hard. Caching asynchronous results is even harder. Learn how to properly cache promise results in JavaScript.
Read article