Running async tasks in series means starting each one only after the previous completes. It's the sequential counterpart to mapAsync — and the contrast between the two reveals how well you understand Promise chaining and concurrency.
What is "async tasks in series"?
Given an array of async functions (tasks), run them one at a time in order. Wait for each to finish before starting the next. Collect all results and return them.
This is the opposite of mapAsync (parallel) and Promise.race (first wins). Series execution is intentionally slower — you use it when:
- Order matters and tasks are dependent — each task needs the previous result
- Rate limiting — hitting an API that allows only one request at a time
- Side effects must be sequential — database migrations, file writes, audit logs
The interview typically asks for three variants:
runSeries(tasks)— array of zero-arg async functions, collect all resultsmapSeries(arr, fn)— likeArray.mapbut async and sequential (mirrorsmapAsync)- Reduce-based implementation — shows you know the functional equivalent
The Problem
"Implement runSeries(tasks) where tasks is an array of async functions. Run them one at a time in order and return an array of results."
The interviewer extends:
"Now implement mapSeries(arr, fn) — same idea but like an async map. Compare it to your parallel mapAsync implementation."
Thought Process
For series execution, a for...of loop with await inside is the canonical solution. Each await suspends the loop until the current task resolves — naturally enforcing sequential execution.
The reduce approach builds a promise chain: each .then() waits for the previous promise to settle before starting the next task. It's functionally identical but shows FP thinking.
Key contrast with parallel:
// Parallel — all tasks start immediately
const results = await Promise.all(tasks.map(t => t()));
// Series — each task waits for the previous
const results = [];
for (const task of tasks) results.push(await task());
Step 1 — runSeries: Sequential Task Runner
Loading editor...
Step 2 — mapSeries: Async Map in Series
mapSeries mirrors Array.map — it takes an array of values and a mapping function, applying the function to each element sequentially:
Loading editor...
Step 3 — Reduce-Based Implementation
The same result expressed as a promise chain — each .then() gates the next task:
Loading editor...
The for...of version is clearer; the reduce version shows functional composition.
Step 4 — Parallel vs Series Comparison
Loading editor...
Full Solution
Loading editor...
What Interviewers Are Testing
awaitinside a loop — knowing thatawaitinsidefor...ofis sequential;Promise.all(arr.map(...))is parallel- Contrast with
mapAsync— being able to explain when to choose series vs parallel - Reduce-based chain — shows you understand that a promise chain is an implicit series
- Order preservation — results array index matches input array index
Complexity
| Time | Space | |
|---|---|---|
runSeries(N tasks) | O(T₁ + T₂ + ... + Tₙ) — sum of all task times | O(N) for results |
mapAsync(N tasks) | O(max(T₁, T₂, ..., Tₙ)) — longest task | O(N) for results |
Interview Tips
- Lead with the trade-off — "Series adds total latency but avoids concurrency. The right choice depends on whether tasks are independent."
- Show
awaitinsidefor...ofexplicitly — some candidates writetasks.forEach(async t => await t())which doesn't serialize —forEachdoesn't await the returned promises. Usefor...of. - Name the reduce pattern — "This is a promise chain — each
.then()gates the next task. The reduce just builds the chain programmatically."