async/await Meets forEach: The Loop That Won't Wait2026-05-02
A backend developer writes a function to process a batch of orders sequentially — each order must be saved to the database before moving to the next, because later orders depend on earlier ones updating inventory counts.
async function processOrders(orders) {
let processed = 0;
orders.forEach(async (order) => {
await validateInventory(order);
await saveToDatabase(order);
processed++;
console.log(`Saved order ${order.id}`);
});
console.log(`Done! Processed ${processed} orders.`);
return processed;
}
// Called as:
const count = await processOrders(pendingOrders);
// count is always 0, and "Done!" prints before any orders are saved
The function appears correct at a glance. It uses async/await inside the loop body. Each iteration awaits both validation and the database write. The developer even tested with a single order and it "worked" — the order did eventually get saved. But in production, processOrders always returns 0, and the "Done!" message appears before any orders are logged as saved.
Array.prototype.forEach completely ignores the return value of its callback. When you pass an async function as the callback, each invocation returns a Promise — and forEach discards every single one. It does not await them. It cannot await them. It was never designed to.
What actually happens at runtime:
forEach calls the async callback for order 1 — gets back a Promise, ignores it.forEach calls the async callback for order 2 — gets back a Promise, ignores it.forEach calls the async callback for order 3 — same thing.forEach returns undefined (synchronously).console.log("Done!") — processed is still 0.0.processed increments… but nobody is listening.This is doubly dangerous: not only does the function report completion prematurely, but the orders — which were supposed to run sequentially — now run concurrently. The inventory checks race against each other, potentially overselling stock.
The fix is to use a for...of loop, which respects await at the statement level:
async function processOrders(orders) {
let processed = 0;
for (const order of orders) {
await validateInventory(order);
await saveToDatabase(order);
processed++;
console.log(`Saved order ${order.id}`);
}
console.log(`Done! Processed ${processed} orders.`);
return processed;
}
Now each iteration fully completes before the next begins, and "Done!" prints only after all orders are saved.
If you actually want concurrency (and your logic supports it), be explicit about it:
await Promise.all(orders.map(async (order) => {
await validateInventory(order);
await saveToDatabase(order);
}));
The same trap applies to .map(), .filter(), and .reduce() — none of them understand Promises. The difference is that .map() at least returns the array of Promises, so you can pass them to Promise.all. forEach returns undefined, making the Promises completely unreachable. No linter warning. No runtime error. Just silent, incorrect behavior.
ESLint's no-return-await rule won't catch this. You need eslint-plugin-promise or the no-await-in-loop rule (inverted logic — it flags the correct code!) to get any static analysis help here.
forEach fires and forgets async callbacks — use for...of for sequential awaiting, or Promise.all(arr.map(...)) for explicit concurrency.
