JavaScript
Promises
Asynchronous
forEach
Coding Tips

Wait for promise in 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

Array.prototype.forEach does not wait for async callbacks, so code after the loop can run before asynchronous work is finished. This leads to partial writes, empty results, or race conditions that are hard to debug. Use control flow patterns that are promise-aware, such as for...of with await or Promise.all for parallel work.

Why forEach and await Clash

forEach ignores return values from the callback, including promises. Even if the callback is marked async, the outer function does not pause for each iteration.

javascript
1async function badExample(items) {
2  const results = [];
3
4  items.forEach(async (item) => {
5    const value = await fetchValue(item);
6    results.push(value);
7  });
8
9  console.log("done length:", results.length);
10  return results;
11}

In this pattern, done length often prints before callbacks complete. The loop schedules work and exits immediately.

Correct Sequential Processing

Use for...of when each step depends on the previous one or when rate limits require strict ordering.

javascript
1async function fetchValue(item) {
2  return new Promise((resolve) => {
3    setTimeout(() => resolve(item * 2), 20);
4  });
5}
6
7async function processSequential(items) {
8  const results = [];
9  for (const item of items) {
10    const value = await fetchValue(item);
11    results.push(value);
12  }
13  return results;
14}
15
16processSequential([1, 2, 3]).then(console.log);

This output order matches input order and completes predictably.

Correct Parallel Processing

If iterations are independent, parallelize with Promise.all. This is usually faster and still easy to reason about.

javascript
1async function processParallel(items) {
2  const promises = items.map((item) => fetchValue(item));
3  return Promise.all(promises);
4}
5
6processParallel([1, 2, 3]).then(console.log);

Use Promise.allSettled when partial failure is acceptable and you need per-item status instead of fail-fast behavior.

Error Handling Strategy

Choose one error model and keep it consistent:

  • Sequential plus try and catch when each step can stop the workflow.
  • Parallel plus Promise.allSettled when you want success and failure reports together.
  • Batched parallelism when full parallel load is too heavy for external APIs.

For production jobs, include item identifiers in errors so failed records can be retried safely.

javascript
1async function processWithStatus(items) {
2  const tasks = items.map(async (item) => {
3    try {
4      const value = await fetchValue(item);
5      return { item, ok: true, value };
6    } catch (err) {
7      return { item, ok: false, error: String(err) };
8    }
9  });
10
11  return Promise.all(tasks);
12}

Controlled Concurrency for Large Workloads

Running every promise at once is risky when arrays are large or external APIs enforce strict rate limits. A practical middle ground is limited parallelism, where you process items in small batches. This preserves throughput while reducing timeout and throttling errors.

javascript
1async function processInBatches(items, batchSize) {
2  const results = [];
3  for (let i = 0; i < items.length; i += batchSize) {
4    const slice = items.slice(i, i + batchSize);
5    const batchResults = await Promise.all(slice.map((item) => fetchValue(item)));
6    results.push(...batchResults);
7  }
8  return results;
9}
10
11processInBatches([1, 2, 3, 4, 5, 6], 2).then(console.log);

This strategy is especially helpful for network-bound jobs such as webhook delivery, file metadata fetches, or API synchronization tasks.

When you review pull requests, check that async loops return or await the final promise chain. Silent fire-and-forget behavior is a common source of data loss in background jobs. Make async intent explicit in function names and comments so callers understand whether a function waits for completion or only schedules work.

Common Pitfalls

  • Expecting await inside forEach to pause the outer function.
  • Using sequential loops for independent work and accepting avoidable latency.
  • Using unbounded Promise.all on huge arrays and overwhelming downstream services.
  • Ignoring error context, which makes retries and incident analysis difficult.
  • Mixing sequential and parallel patterns in one function without clear intent.

Summary

  • forEach is not promise-aware and should not drive awaited control flow.
  • Use for...of with await for ordered sequential processing.
  • Use Promise.all for parallel independent tasks.
  • Choose explicit error behavior with try and catch or settled results.
  • Keep concurrency strategy intentional to avoid races and throughput issues.

Course illustration
Course illustration

All Rights Reserved.