ForEach Causing Issues When Used with Async Callbacks? Here's Why

While it's not entirely accurate to say that forEach "causes problems" with async functions, it just ignores the fact that you used an async function as your callback.

Regardless of whether the callback function is async or not, forEach iterates through the array synchronously, without regard to any asynchronous operations within the callback.

This means that even if the callback returns promises from async functions, forEach won't wait for them to resolve.

To illustrate this, consider the following simpler example:

[1, 2, 3].forEach(async (n) => {
  console.log("callback start");
  await new Promise((resolve) => setTimeout(resolve, n * 1000));
  console.log("callback end");
});
console.log("forEach complete");

/*
Output
callback start
callback start
callback start
forEach complete
callback end
callback end
callback end
*/

As you can see, the forEach completes execution before any of the async operations within its callbacks are finished.

This behavior extends to other array methods as well; none of them inherently support async operations or return promises to indicate when they've completed.

An alternative approach is to use traditional loops like for or for...of, which support asynchronous operations by pausing execution until the awaited operation completes:

for (const n of [1, 2, 3]) {
  console.log("callback start");
  await n;
  console.log("callback end");
}
console.log("for...of complete");

// callback start
// callback end
// callback start
// callback end
// callback start
// callback end
// for...of complete

In this example, each iteration waits for the asynchronous operation to complete before moving to the next one.

If you need concurrent execution of promises, you can use map, which collects all the promises returned by the callback functions and allows you to wait for them using Promise.all:

const promises = [1, 2, 3].map(async (n) => {
  console.log("callback start");
  await new Promise((resolve) => setTimeout(resolve, n * 1000));
  console.log("callback end");
});
await Promise.all(promises);
console.log("map (all) complete");

// callback start (x3)
// callback end (x3)
// map (all) complete

I have created a set of resources for learning asynchronous JavaScript. These guides—Callbacks, Promises, and Async/Await —cover everything I’ve learned from years of real-world JavaScript experience.

If you found this article helpful, you’ll get so much out of these guides. Each one is optimized for those “lightbulb moments,” building a strong mental model for how asynchronous JavaScript works and how you can use it to create fast, dynamic applications.