
The trap everyone hits once
You have an array of items and you need to await something for each one. The naive version looks perfectly reasonable — until it misbehaves:
async function processAll(items) {
items.forEach(async (item) => {
const res = await fetchItem(item); // looks sequential...
console.log(res);
});
console.log("done"); // prints BEFORE the logs above
}
Run it and you’ll see "done" logged before any result, and the items may finish out of order. Here’s why.
What’s actually happening
forEachis a synchronous loop that calls your callback once per element and ignores the callback’s return value — including a returned promise.- Your
asynccallback spins up a promise and immediately returns;forEachnever awaits it, so it races on to the next element (and then to"done"). - All callbacks run in the same microtask tick, so their
awaitpoints resolve independently — order is not guaranteed.
Fix 1 — sequential, in order: for...of
When you need each item processed one after another (e.g. rate-limited APIs), use a real loop:
async function processAll(items) {
for (const item of items) {
const res = await fetchItem(item);
console.log(res);
}
console.log("done"); // now prints LAST, in order
}
Fix 2 — parallel, fastest: map + Promise.all
When order matters but you want concurrency, collect promises and await them together:
async function processAll(items) {
const results = await Promise.all(
items.map((item) => fetchItem(item))
);
results.forEach((res) => console.log(res)); // ordered, all settled
}
Fix 3 — fire-and-forget with a guard
If you truly don’t need to wait, at least catch errors instead of swallowing them:
items.forEach((item) => {
fetchItem(item).then(console.log).catch((e) => console.error(e));
});
Rule of thumb
forEach was designed for synchronous side effects. For async work, reach for for...of (sequential) or map+Promise.all (parallel). Your awaits will finally do what you expect.