Topics

Jump to a topic.

Search questions

Find a question by title, tag, or summary.

Sequential vs parallel await — and await inside loops?

Questions in this chapter (7)
  • Draft
  • medium
  • Askedvery often
  • Verified October 2026

Answer

An await pauses the current function until a promise settles. Whether operations overlap depends on when each promise is created, because a promise represents work that has already started.

  • Sequential. for (const x of xs) await work(x) calls work for the next item only after the previous one has finished. Total time is the sum. Use it when each step needs the previous result, or when order and rate matter.
  • Parallel. await Promise.all(xs.map(work)) calls work for every item first, which starts all operations, then waits for all of them. Total time is roughly the slowest one. The results keep input order.
  • Also parallel. Create the promises first, then await them one by one: const a = f(); const b = g(); await a; await b;. The combinator is only a convenient way to wait. The danger is below.
  • forEach does not wait. It calls the callback and ignores the returned promise. Code after forEach runs before the async callbacks finish, and errors in them become unhandled rejections.
  • for await (const x of source) waits for each item in turn. Use it for async iterables, such as streams and paginated sources. If the source is a sync iterable of promises, it awaits each one in order, but the promises already run.
  • Unbounded parallelism is not free. Promise.all(10_000_items.map(fn)) starts everything at once. Limit concurrency instead (see the task runner question).

Pick by dependency: dependent steps are sequential, independent steps are parallel.

Example

const delay = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
const timed = async (label, fn) => {
  const start = performance.now();
  const result = await fn();
  const elapsed = Math.round((performance.now() - start) / 100) * 100;
  console.log(label, JSON.stringify(result), `~${elapsed} ms`);
};
const ids = [1, 2, 3];
const fetchUser = (id) => delay(100, `user${id}`);

await timed('sequential', async () => {
  const users = [];
  for (const id of ids) users.push(await fetchUser(id));
  return users;
});
await timed('parallel  ', () => Promise.all(ids.map(fetchUser)));
// → sequential ["user1","user2","user3"] ~300 ms
// → parallel   ["user1","user2","user3"] ~100 ms

Pitfalls

  • Awaiting in a loop when the iterations are independent. It is correct but slow, and a frequent review comment.
  • Using async callbacks with forEach. The loop finishes immediately, so the code after it sees unfinished work. Use for...of to run in order or Promise.all(xs.map(...)) to run in parallel.
  • Creating promises early and awaiting them late. A promise that rejects before you reach its await is unhandled until then. In Node.js the default mode raises an unhandled rejection as an uncaught exception, which can crash the process. Attach handlers right away, or use Promise.all/allSettled.
  • Assuming Promise.all cancels the other work when one fails. It only stops waiting. See the cancellation question.

Follow-ups

Does Promise.all(xs.map(async (x) => ...)) run in parallel?
Yes. map calls each async callback synchronously up to its first await, which starts all the work. all then waits for the returned promises.
How do you process in parallel but stop at the first failure?
Promise.all rejects at the first rejection, but the other work keeps running. To also stop starting more work, use a pool with a stop flag, as in the task runner question, and pass an AbortSignal to the work you can cancel.
Sequential or parallel for database writes that depend on order?
Sequential. Parallel calls can finish in any order, and the server may handle them in any order too. Parallel await only guarantees the order of the result array, not of the effects.

Exercises

Does `forEach` wait

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
[1, 2, 3].forEach(async (id) => {
  await delay(10);
  console.log('done', id);
});
console.log('after forEach');

Parallel without `Promise.all`

const delay = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
const start = performance.now();
const a = delay(100, 'a');
const b = delay(100, 'b');
const results = [await a, await b];
console.log(results, `~${Math.round((performance.now() - start) / 100) * 100} ms`);

The early rejection

const delay = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
const slow = delay(50, 'slow');
const fast = Promise.reject(new Error('fast failure'));
try {
  console.log(await slow);
  console.log(await fast);
} catch (error) {
  console.log('caught', error.message);
}