loop
forEach
await
async

Using async/await with a forEach loop

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

async and await do not work with forEach the way many developers expect. The callback can be async, but forEach itself does not wait for those promises, so the outer function moves on immediately.

Why forEach Does Not Await

forEach was designed for synchronous callbacks. It ignores the promise returned by an async callback, which means completion order and error propagation are often not what you intended.

javascript
1function wait(ms) {
2  return new Promise(resolve => setTimeout(resolve, ms));
3}
4
5async function demo() {
6  const values = [1, 2, 3];
7
8  values.forEach(async value => {
9    await wait(100);
10    console.log("processed", value);
11  });
12
13  console.log("done");
14}
15
16demo();

The output prints done before the looped work finishes. That is the core problem. The callback awaits internally, but the surrounding control flow never waits for all callbacks to settle.

Use for...of for Sequential Work

If each operation should run one after another, use for...of. It is the clearest pattern when order matters or when the next step depends on the previous result.

javascript
1function wait(ms) {
2  return new Promise(resolve => setTimeout(resolve, ms));
3}
4
5async function processSequential(values) {
6  for (const value of values) {
7    await wait(100);
8    console.log("processed", value);
9  }
10
11  console.log("done");
12}
13
14processSequential([1, 2, 3]);

This guarantees ordered execution and predictable error behavior. If one iteration throws, the loop stops unless you catch the error inside the body.

Use Promise.all for Parallel Work

If the operations are independent, map the items to promises and await them together.

javascript
1function wait(ms) {
2  return new Promise(resolve => setTimeout(resolve, ms));
3}
4
5async function processParallel(values) {
6  await Promise.all(
7    values.map(async value => {
8      await wait(100);
9      console.log("processed", value);
10      return value * 2;
11    })
12  );
13
14  console.log("done");
15}
16
17processParallel([1, 2, 3]);

This is usually faster for network calls or other I/O-bound work, because the tasks overlap instead of waiting in line.

If you need every result even when some fail, switch to Promise.allSettled and inspect each outcome explicitly.

Limit Concurrency When Needed

Running everything at once can overload an API, database, or filesystem. In those cases, use a small worker pool instead of forEach.

javascript
1async function mapWithLimit(items, limit, worker) {
2  const results = new Array(items.length);
3  let index = 0;
4
5  async function runner() {
6    while (index < items.length) {
7      const current = index;
8      index += 1;
9      results[current] = await worker(items[current]);
10    }
11  }
12
13  await Promise.all(Array.from({ length: limit }, runner));
14  return results;
15}
16
17mapWithLimit([1, 2, 3, 4], 2, async value => {
18  await new Promise(resolve => setTimeout(resolve, 100));
19  return value * 10;
20}).then(console.log);

That pattern gives you backpressure without losing the benefits of async code.

Common Pitfalls

The biggest mistake is assuming await inside forEach makes the whole loop awaitable. It does not. If the outer function must wait, use for...of, Promise.all, or another explicit promise-based pattern.

Another common bug is unhandled rejections. Because forEach does not collect the promises, errors from callbacks may bypass your surrounding control flow and appear later as unhandled promise warnings.

Developers also mix up ordering guarantees. Promise.all waits for all tasks to finish, but it does not make them run sequentially. If the order of side effects matters, use for...of instead.

Finally, full parallelism is not always safe. If you launch hundreds of requests at once, you can hit rate limits or exhaust resources. Add concurrency control when the destination system cannot handle a burst.

Summary

  • 'forEach does not wait for promises returned by an async callback.'
  • Use for...of when the work must happen sequentially.
  • Use Promise.all when the work can happen in parallel.
  • Use Promise.allSettled or a worker-pool pattern when failure handling or concurrency limits matter.
  • Treat loop structure as part of async control flow, not just as a stylistic choice.

Course illustration
Course illustration

All Rights Reserved.