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

Get my free, weekly JavaScript tutorials

Want to improve your JavaScript fluency?

Every week, I send a new full-length JavaScript article to thousands of developers. Learn about asynchronous programming, closures, and best practices — as well as general tips for software engineers.

Join today, and level up your JavaScript every Sunday!

Thank you, Taha, for your amazing newsletter. I’m really benefiting from the valuable insights and tips you share.

- Remi Egwuda