N Tasks in Parallel
beginnerFire all tasks at once and gather results — plus what parallel really means on a single thread.
Run async tasks strictly one after another in JavaScript — the async/await loop, the reduce promise-chain, why spreads make it O(n²), and when series is correct.
Given an array of task functions (each returning a promise), run them strictly one after another — task 2 must not start until task 1 settles — and resolve with all results in order.
This is the beginner end of the concurrency-shape family (parallel, race, batches, pool), and its two learning goals are: tasks must be functions (a promise is already running — you can't sequence what's already started), and sequencing = chaining, not waiting.
async function runInSeries(tasks) {
const results = [];
for (const task of tasks) {
results.push(await task()); // start next ONLY after previous settles
}
return results;
}That's it. await inside a for...of loop is series execution — the loop body can't continue until the promise settles. A rejection anywhere stops the run and rejects runInSeries's promise with that error — usually exactly what you want for dependent steps, and if it isn't, wrap individual tasks (see below) rather than silently converting failures to null.
Note this is the one place where the lint rule no-await-in-loop is wrong: the rule exists to catch accidental serialization of independent work; here serialization is the requirement. Saying that shows you know why the rule exists rather than just obeying it.
Before async/await, the idiom was folding the tasks into one chain — and interviewers still ask for it by name:
function runInSeries(tasks) {
const results = [];
return tasks
.reduce(
(chain, task) => chain.then(() => task()).then((r) => results.push(r)),
Promise.resolve()
)
.then(() => results);
}The accumulator is a promise, and each fold step extends the chain: p.then(t1).then(t2)... built programmatically. Understanding why this works — reduce doesn't know about promises at all; it's just building a value that happens to be a chain — is the conceptual payoff.
One performance note that doubles as a code-review signal: the popular variant chain.then((acc) => task().then((r) => [...acc, r])) copies the whole results array on every step — O(n²) for something a push does in O(n). Harmless at n=5, real at n=10,000.
const tasks = [
() => delay(300, "A"),
() => delay(100, "B"),
() => delay(200, "C"),
];
await runInSeries(tasks); // ["A", "B", "C"] after ~600ms
// t=0 task A starts (B and C are functions — nothing running yet)
// t=300 A settles → B starts
// t=400 B settles → C starts
// t=600 C settles → resolve ["A", "B", "C"]
// Total = SUM of durations. Parallel would be MAX (~300ms) — that's the trade.If none of these hold, series is just slow — the follow-up "why is this taking 6 seconds?" is asking you to notice the tasks were independent all along.
[]; both versions handle it naturally (loop never runs; reduce returns the initial promise).results.push(await task().catch((e) => ({ error: e }))) — outcome objects, not silent nulls that later blow up as Cannot read properties of null.await task() catches it (the throw happens inside the async function); the raw reduce version also routes it into the chain because the throw happens inside a then callback. Worth one sentence.typeof t === "function" ? t() : t) or throw. State the policy.[...acc, r] spreads in the reduce — quadratic.forEach(async (t) => await t()) — forEach ignores the returned promises; all tasks fire concurrently and nothing is awaited. A classic bug worth being able to explain, not just avoid.console.error + null inside the utility — swallowing errors is a policy decision the caller should make.acc = await task(acc) — the fold becomes a pipeline; compare pipe.await in a loop serialize?" — each await suspends the async function until the microtask resumes it; see the event loop.next(i) function; good for checking the recursion is tail-shaped and error-propagating.Fire all tasks at once and gather results — plus what parallel really means on a single thread.
Process a large list of async tasks in fixed-size sequential batches to protect downstream services.
First settled task wins: build the primitive behind timeouts and fastest-source fetching.
Retry a failing async operation N times with backoff — a small function with big production implications.