DEV Community

Cover image for Why Async/Await Inside forEach Is a Bad Idea
Maxim Orlov
Maxim Orlov

Posted on • Edited on • Originally published at maximorlov.com

Why Async/Await Inside forEach Is a Bad Idea

This article was originally published at https://maximorlov.com/async-await-inside-foreach/

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);
Enter fullscreen mode Exit fullscreen mode

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. 👀

Note: The order in which the requests finish is random and will rarely be the same order in which they were sent out. This is another reason not to use async/await inside forEach to populate an array — the arrangement of the results will differ from the original array.

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);
Enter fullscreen mode Exit fullscreen mode

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

Get the FREE guide

Top comments (0)