JavaScript async loop illustration

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

  • forEach is a synchronous loop that calls your callback once per element and ignores the callback’s return value — including a returned promise.
  • Your async callback spins up a promise and immediately returns; forEach never awaits it, so it races on to the next element (and then to "done").
  • All callbacks run in the same microtask tick, so their await points 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.