Why JavaScript forEach() Does Not Work with await (How to Fix It)
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 returnsundefined. - Even if the callback is
async,forEach()does not pause for the promise to resolve. - Consequently,
awaitinside 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()ignoresawaitand returnsundefined.- Use
for...ofwhen you need sequential async execution. - Use
Promise.all(withmap) 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.