Use async and await when iterating over an array

Choose sequential loops, concurrent promise collection, or bounded concurrency instead of expecting forEach to await an async callback.

Best practices
Illustration accompanying a Node.js development guide

forEach() calls each callback without waiting for its returned promise. An async callback is legal, but await array.forEach(...) does not wait for the asynchronous work because forEach() returns undefined.

Why completion appears out of order

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
[1, 2, 3].forEach(async (number) => {
  await delay(number * 1000);
  console.log(number);
});
console.log('Loop returned');

The last line appears first. The callbacks then typically print 1, 2, and 3 after roughly one, two, and three seconds. They started their timers without waiting for one another; this is not a six-second sequential loop. A rejected callback promise also needs explicit handling because forEach does not collect it.

Process items sequentially

async function processInOrder(items, processItem) {
  const results = [];
  for (const item of items) {
    results.push(await processItem(item));
  }
  return results;
}

Use this when the next operation depends on the previous one, order matters, or the service should receive only one request at a time. A rejection exits the loop unless you handle it inside the loop. A traditional indexed for loop is another option; for...in enumerates property keys and is not the usual array-value loop.

Run independent operations concurrently

async function processTogether(items, processItem) {
  return Promise.all(items.map((item) => processItem(item)));
}

Calling processItem starts the work; Promise.all collects its outcomes. Results remain in input order even when operations finish out of order. The aggregate rejects when an input rejects, but other operations keep running unless you cancel them through a supported mechanism.

Use Promise.allSettled() to receive a success or failure record for every input. For large arrays, limit concurrency rather than launching an unbounded number of requests. A limiter such as p-limit can schedule promise-returning functions with a chosen maximum number in flight.

Async filtering and reduction

An async predicate returns a promise, which is truthy. Passing one directly to filter() therefore does not filter according to the eventual Boolean result:

async function filterAsync(items, predicate) {
  const keep = await Promise.all(items.map(predicate));
  return items.filter((item, index) => keep[index]);
}

This version evaluates predicates concurrently. For a sequential async reduction, await the accumulated promise before using its value, or write an explicit for...of loop for clarity.

for await...of consumes async iterables and also awaits values from synchronous iterables. It does not make promises that you already created start sequentially. If a task truly is fire-and-forget, attach deliberate error handling and consider whether the process may exit before it finishes.

See MDN's forEach reference, Promise.all, and p-limit.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting