beginnerCommon4 min read · Updated Jul 18, 2026

Run N Async Tasks in Parallel

Run async tasks concurrently in JavaScript, aggregate results with Promise.all or allSettled — and explain what 'parallel' actually means on a single thread.


The problem

Given an array of task functions, start them all at once, wait for all to finish, and resolve with results in order.

The implementation is two lines. The question survives in interviews because of the sentence hiding behind it — the one the interviewer actually wants:

JavaScript is single-threaded, so your code never runs in parallel — but the waiting does. fetch, timers, and disk I/O happen off-thread (network stack, OS); "parallel tasks" means N I/O operations in flight while the one JS thread handles completions one at a time. That's concurrency, and it's why this pattern gives near-Nx speedups for I/O-bound work and roughly zero for CPU-bound work — N compute-heavy tasks still execute sequentially on the same thread, just interleaved. (True CPU parallelism needs Web Workers.) Deliver that distinction unprompted and this question is over.

Implementation

function runInParallel(tasks) {
  return Promise.all(tasks.map((task) => task()));
}
  • tasks.map((task) => task()) — this line starts every task; by the time Promise.all sees the array, all N are in flight. all doesn't run anything — it only aggregates (details).
  • Results come back in input order regardless of completion order — Promise.all's job.
  • Total time ≈ the slowest task (vs the sum for series).

The sequencing proof interviewers ask for

// PARALLEL — both timers overlap: total ≈ 300ms
const [a, b] = await Promise.all([delay(300, "A"), delay(200, "B")]);
 
// SEQUENTIAL BY ACCIDENT — the classic await bug: total ≈ 500ms
const a = await delay(300, "A"); // nothing else is even started yet
const b = await delay(200, "B");
 
// The fix that keeps await syntax: start first, await after
const pA = delay(300, "A");
const pB = delay(200, "B"); // both running now
const a = await pA;
const b = await pB;         // total ≈ 300ms

The "start promises first, await them together" pattern is the practical takeaway — it's how you parallelize inside an async function without restructuring into Promise.all (though Promise.all also surfaces rejections better; two sequential awaits on already-started promises can leave a rejection momentarily unhandled — a nuance worth mentioning, not deep-diving).

Error handling is a contract decision

Promise.all is all-or-nothing: first rejection rejects the aggregate (while other tasks keep running — no cancellation). If partial results are acceptable, don't hand-roll catch → null:

// ❌ the anti-pattern: null poisons downstream code, error details lost to console
tasks.map((t) => t().catch((e) => { console.error(e); return null; }))
 
// ✅ the standard tool: per-task outcomes, nothing swallowed
const outcomes = await Promise.allSettled(tasks.map((t) => t()));

allSettled (implementation) exists precisely so utilities don't invent lossy error conventions. "All-or-nothing → all; partial tolerance → allSettled; and the choice belongs to the caller" is the senior phrasing.

Edge cases interviewers probe

  • Unbounded fan-outrunInParallel(tenThousandTasks) opens 10,000 connections; browsers cap per-origin requests (~6 on HTTP/1.1) and servers rate-limit you. The production answer is a concurrency cap: the promise pool. Naming the limit before being asked is the difference between knowing the API and having operated it.
  • Empty array → resolves [] immediately (inherited from Promise.all).
  • CPU-bound tasks — no speedup (see above); the fix is Workers, not promises.
  • A task throws synchronouslytask() inside map throws before Promise.all is even called, escaping as a sync exception. Wrap with Promise.resolve().then(task) if tasks are untrusted.
  • "Are they guaranteed to start in order?" — the starts are synchronous in array order; the completions are not. Both halves of that sentence get tested.

Common mistakes

  • await in a loop when tasks are independent (accidental series — the most common real-world perf bug in async code, and the reason the no-await-in-loop lint rule exists).
  • forEach(async ...) — starts everything but awaits nothing; the function returns before any task finishes.
  • catch → null inside the utility instead of allSettled.
  • Saying "Promise.all runs my tasks in parallel" — it aggregates; map calling task() is what started them.
  • No concurrency bound for large inputs.

Test yourself

Predict the outputRoughly how long does run() take?
const delay = (ms, v) =>
  new Promise((res) => setTimeout(() => res(v), ms));
async function run() {
  const a = delay(300, "A");
  const b = delay(200, "B");
  console.log(await a, await b);
}
run();

Follow-up questions

  • "Cap it at K concurrent." — the pool; this is the canonical escalation.
  • "First success instead of all?"race / Promise.any.
  • "How does the single thread handle 100 completions?" — completion callbacks queue as microtasks and run one at a time: the event loop.
  • "Parallelize CPU work?" — Web Workers + postMessage; promises coordinate, workers compute.
  • "Cancel the rest when one fails?" — pass one AbortSignal to every task and abort it in a catch — promises alone can't stop work.

  • First settled task wins: build the primitive behind timeouts and fastest-source fetching.

  • Run async tasks one after another and understand why reduce-with-promises works.

  • Convert error-first callback APIs into promise-returning functions, like Node's util.promisify.

  • Batching Promises

    intermediate

    Process a large list of async tasks in fixed-size sequential batches to protect downstream services.