forEach Doesn't Await Anything
An async callback inside forEach does not wait for anything. Your loop finishes before the work does — and because the code looks right, this is one of the most common async bugs an AI assistant will happily write for you.
Why forEach can't wait
MDN says it plainly: "forEach() expects a synchronous function — it does not wait for promises." An async function always returns a promise, but forEach throws that return value away. It calls your callback once per element, back to back, and moves on. The result:
const ratings = [5, 4, 5];
let sum = 0;
ratings.forEach(async (rating) => {
sum = await sum + rating;
});
console.log(sum); // 0 — not 14
Every callback is started; none is finished before the console.log runs. Worse failure modes follow the same shape: DB writes that land after the response is sent, files closed while still being written, try/catch around the loop that catches nothing because rejections escape as unhandled promise rejections.
Pick your intent: two patterns that actually wait
The fix is not a clever wrapper around forEach — it's stating which behavior you want.
Sequential — order and back-pressure matter. Use for...of with await inside. Each iteration finishes before the next starts, errors surface at the await, and you never fire 10,000 requests at once:
for (const user of users) {
await sendEmail(user); // one at a time, in order
}
Concurrent — order doesn't matter. Use Promise.all over a map. All the work starts immediately, and the surrounding function pauses until every promise settles:
const results = await Promise.all(users.map((u) => fetchProfile(u.id)));
Promise.all is fail-fast: it rejects as soon as any input promise rejects, with that first error. When you need every outcome even if some fail, use Promise.allSettled, which always waits and returns {status, value|reason} per item.
Rules of thumb
| Situation | Use | Why |
|---|---|---|
| Order matters, or each step depends on the last | for...of + await | Strictly sequential, natural error handling |
| Independent work, want it fast, one failure should abort | await Promise.all(items.map(fn)) | Concurrent, fail-fast |
| Independent work, need every result even on failures | await Promise.allSettled(items.map(fn)) | Never short-circuits |
| Fire-and-forget side effects on a sync value | forEach (non-async callback) | Fine — nothing to await |
Any async callback | never forEach | The promise is discarded |
Power-user notes
- Bounded concurrency.
Promise.allon 5,000 items launches 5,000 requests at once. Chunk the array andPromise.alleach chunk inside afor...of, or use a small pool utility — you get concurrency and back-pressure. map+ noawaitis the same bug.items.map(async ...)withoutawait Promise.all(...)produces an array of pending promises you never look at. Themapisn't the fix; theawait Promise.allis.for await...ofis for async iterables (streams, paginated APIs) — sources that produce values asynchronously. For a plain array of items you process asynchronously, plainfor...ofwithawaitinside is the right tool.- Lint it. Reviewing AI-generated code, grep for
forEach(async— it is almost always wrong. Rules likeno-misused-promises(typescript-eslint) flag promises passed wherevoidis expected.
Resources
- Array.prototype.forEach() — MDN — the "expects a synchronous function" warning and the
sum === 0example - Promise.all() — MDN — concurrency + fail-fast rejection semantics
- Promise.allSettled() — MDN — wait for every outcome
- for...of — MDN — the sequential pattern
- for await...of — MDN — iterating async iterables
Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.