N Tasks in Race
beginnerFirst settled task wins: build the primitive behind timeouts and fastest-source fetching.
Run async tasks concurrently in JavaScript, aggregate results with Promise.all or allSettled — and explain what 'parallel' actually means on a single thread.
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.
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).Promise.all's job.// 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 ≈ 300msThe "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).
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.
runInParallel(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.[] immediately (inherited from Promise.all).task() inside map throws before Promise.all is even called, escaping as a sync exception. Wrap with Promise.resolve().then(task) if tasks are untrusted.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.map calling task() is what started them.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();postMessage; promises coordinate, workers compute.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.
Process a large list of async tasks in fixed-size sequential batches to protect downstream services.