Why JavaScript forEach() Does Not Work with await (And How to Fix It)
One of the most confusing async problems in JavaScript happens when developers use await inside forEach() and expect everything to run in order.
It looks correct.
It feels correct.
But it breaks in production.
This issue appears often in API requests, database operations, email sending, payment processing, and file uploads.
Many developers lose hours debugging this.
Let’s fix it properly.
The Problem
Suppose you want to process users one by one.
You write this:
const users = ["John", "Emma", "Michael"];
users.forEach(async (user) => {
await sendEmail(user);
console.log(`Email sent to ${user}`);
});
console.log("All emails processed");
At first glance, this looks perfect.
You expect:
- Send email to John
- Send email to Emma
- Send email to Michael
- Print final message
Expected output:
Email sent to John
Email sent to Emma
Email sent to Michael
All emails processed
But the actual output is often:
All emails processed
Email sent to John
Email sent to Emma
Email sent to Michael
Sometimes even worse—execution becomes unpredictable.
Very frustrating.
Why This Happens
The problem is simple:
forEach() does not wait for async/await.
It completely ignores Promises.
Even if you write await inside it, forEach() itself does not pause.
It immediately starts all iterations and moves on.
That means this line:
console.log("All emails processed");
runs before the async work finishes.
This creates race conditions and broken logic.
Especially dangerous in production systems.
Understanding the Core Issue
Let’s simplify it.
This:
users.forEach(async (user) => {
await sendEmail(user);
});
does NOT mean:
“Wait for each task one by one”
It actually means:
“Start everything immediately and do not wait”
That is the real problem.
forEach() was designed for synchronous operations—not async control flow.
Wrong Real-World Example
Imagine payment processing:
orders.forEach(async (order) => {
await chargeCustomer(order);
});
closeDailyReport();
Danger:
The report may close before payments finish.
That can create serious business problems.
This is not a small bug.
It can affect money.
The Correct Fix: Use for...of
The safest solution is for...of.
Like this:
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();
Now JavaScript waits properly.
Output:
Email sent to John
Email sent to Emma
Email sent to Michael
All emails processed
Perfect.
Predictable.
Safe.
Why for...of Works
Because for...of respects await.
Each loop waits for the previous one to finish.
This is called sequential execution.
Very useful for:
- Payment processing
- Database writes
- Email sending
- File uploads
- Rate-limited APIs
- Authentication flows
Anywhere order matters.
What If You Want Parallel Execution?
Sometimes you do want everything to run together.
For example:
- Fetching multiple products
- Loading dashboard widgets
- Getting multiple API responses
In that case, use Promise.all().
Example:
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");
This runs tasks in parallel—but still waits for all of them to finish.
Very powerful.
Rule to Remember
Use this simple rule:
Need order?
Use for...of
Need speed?
Use Promise.all()
Never use?
forEach() with await
This rule saves a lot of debugging time.
Another Common Mistake
Developers often write this:
await users.forEach(async (user) => {
await sendEmail(user);
});
This still does NOT work.
Why?
Because forEach() returns undefined.
There is nothing real for await to wait for.
So even this fails.
Very common mistake.
Debugging Question
Whenever async loops behave strangely, ask:
“Am I awaiting the loop… or only the function inside it?”
That question reveals the bug fast.
Most developers miss this.
Final Thoughts
forEach() is excellent for simple synchronous loops.
But for async workflows, it becomes dangerous.
Remember:
-
forEach()ignoresawait -
for...ofhandles sequence safely -
Promise.all()handles parallel work properly -
await forEach()is still wrong
Understanding this makes JavaScript async code much cleaner.
And production bugs become much easier to prevent.
Your Turn
Have you ever used await inside forEach() and spent hours debugging it?
Almost every JavaScript developer has.
Peace,
Emily Idioms