Why JavaScript forEach() Does Not Work with await (How to Fix It)

Published: (April 20, 2026 at 08:24 AM EDT)
3 min read
Source: Dev.to

Source: Dev.to

The Issue: await Inside Array.prototype.forEach

One of the most confusing async problems in JavaScript occurs when developers place await inside a forEach() callback and expect the iterations to run sequentially.

const users = ["John", "Emma", "Michael"];

users.forEach(async (user) => {
  await sendEmail(user);
  console.log(`Email sent to ${user}`);
});

console.log("All emails processed");

What you expect

Email sent to John
Email sent to Emma
Email sent to Michael
All emails processed

What actually happens

All emails processed
Email sent to John
Email sent to Emma
Email sent to Michael

forEach() does not wait for the async work inside its callback. It starts all iterations immediately and then moves on, so the console.log("All emails processed") runs before any sendEmail promises settle. This leads to race conditions and broken logic—especially dangerous in production code such as payment processing, database writes, or email campaigns.

Why forEach() Doesn’t Work With await

  • forEach() was designed for synchronous operations.
  • The callback’s return value is ignored; forEach() itself returns undefined.
  • Even if the callback is async, forEach() does not pause for the promise to resolve.
  • Consequently, await inside the callback has no effect on the outer flow.
// This does NOT mean “wait for each task one by one”
users.forEach(async (user) => {
  await sendEmail(user);
});

It actually means “start everything immediately and do not wait”.

Safe Sequential Execution with for...of

The simplest way to process items one after another is to use a for...of loop inside an async function.

const users = ["John", "Emma", "Michael"];

async function processUsers() {
  for (const user of users) {
    await sendEmail(user);
    console.log(`Email sent to ${user}`);
  }

  console.log("All emails processed");
}

processUsers();

Output

Email sent to John
Email sent to Emma
Email sent to Michael
All emails processed

for...of respects await, ensuring each iteration finishes before the next begins. This pattern is ideal for:

  • Payment processing
  • Database writes
  • Email sending
  • File uploads
  • Rate‑limited APIs
  • Authentication flows
  • Any scenario where order matters

Parallel Execution with Promise.all

When order is irrelevant and you want to run tasks concurrently, use Promise.all with Array.prototype.map.

const users = ["John", "Emma", "Michael"];

await Promise.all(
  users.map(async (user) => {
    await sendEmail(user);
    console.log(`Email sent to ${user}`);
  })
);

console.log("All emails processed");

All sendEmail calls start at once, but await Promise.all ensures the final log runs only after all promises settle.

Common Mistake: await on forEach

Some developers try to await the forEach call itself:

await users.forEach(async (user) => {
  await sendEmail(user);
});

This still fails because forEach() returns undefined; there is nothing for await to wait on.

Rule of thumb:
Never use await with forEach. Use for...of for sequential work or Promise.all for parallel work.

Summary

  • forEach() ignores await and returns undefined.
  • Use for...of when you need sequential async execution.
  • Use Promise.all (with map) for parallel async execution.
  • Remember: “Am I awaiting the loop, or only the function inside it?”—this question quickly reveals the bug.

Understanding these patterns makes JavaScript async code cleaner and helps prevent hard‑to‑track production bugs.

0 views
Back to Blog

Related posts

Read more »

Master Destructuring in One Go

What Destructuring Means Destructuring is a JavaScript expression that lets you extract values from arrays, objects, maps, and sets in a concise way. Instead o...