Skip to content
TaeyoungKim.dev

Why JavaScript forEach does not await async callbacks: for...of vs Promise.all

WebWritten 3 min readTaeyoungKim
LinkedInX

You save three products, then display “all done.” Yet the completion message appears before any save finishes. The code contains await, so what happened? The key question is who awaits the Promises returned by the callbacks.

The saveItem() function below simulates a server save with a short delay. We will use the same IDs, 1, 2, 3, to compare forEach, for...of, and Promise.all.

Why does the message after forEach(async ...) appear first?

forEach calls the callback for every element and returns undefined. It does not collect and await the Promises returned by an async callback. An await inside that callback only suspends that callback. MDN documents this behavior.

The diagram compares the completion boundary. In this example the deliberately different delays make saves finish in the order 3, 2, 1; for...of with await moves to the next ID only after the previous save completes.

javascript
const ids = [1, 2, 3];
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function saveItem(id) {
  await sleep((4 - id) * 10);
  console.log(`saved:${id}`);
  return id;
}

ids.forEach(async (id) => {
  await saveItem(id);
});
console.log('all done?');

Here all done? prints first, followed by saved:3, saved:2, and saved:1. The delays were chosen to expose the ordering difference; real network completion order is not predictable. Adding await before ids.forEach(...) does not help because forEach returns undefined, not a Promise representing the saves. Errors from those callbacks also need their own handling.

When order matters, what does for...of await?

Keep ids and saveItem() from the first example, but replace the loop:

javascript
for (const id of ids) {
  await saveItem(id);
}
console.log('all done');

Now the output is saved:1 → saved:2 → saved:3 → all done. Each iteration finishes before the next starts. Use this when a later save depends on an earlier one or when request order is part of the contract. Independent requests take longer when processed strictly one at a time.

If saveItem(id) rejects, the loop exits and control reaches an outer catch unless you handle the error inside the loop. To attempt the remaining IDs, use per-item try/catch and record failures. Do not silently display “all done” if some saves failed.

How do you start independent work and await all of it?

When save order does not matter but every result does, create the Promises and await their aggregate:

javascript
const results = await Promise.all(ids.map((id) => saveItem(id)));
console.log('all done');
console.log(results); // [1, 2, 3]

All three saves start before Promise.all settles. They may finish 3, 2, 1, but results retains input order. If one rejects, the aggregate rejects; the other saves that have already started are not automatically cancelled. Retrying the whole group can repeat successful writes, so a real save API needs a duplicate or idempotency policy. For thousands of requests, also limit concurrency instead of starting all of them at once.

Which iteration matches the desired completion rule?

Desired behaviorChoiceMeaning of completion
Start each job without awaiting its resultforEach callbackLoop end does not mean jobs are done
Start the next job after the previous one finishesfor...of + awaitAfter the last awaited job
Start independent jobs together and wait for allPromise.all(ids.map(...))After every Promise fulfills

Before sending real requests, decide whether order matters, what to do when one fails, and whether a retry can duplicate a write. The right loop follows from those requirements.

Key takeaways

The await in forEach(async ...) waits only inside each callback. The surrounding forEach does not await those callbacks, so a completion message can run first. Use for...of for sequential work and Promise.all to await independent work together. Define failure and retry behavior before claiming that everything has finished.

Author

TaeyoungKim

Connecting technical foundations with implementation, verification, and production decisions.

Read next