Implement Async Tasks in Series

Run async functions one at a time in order. Implement runSeries and mapSeries with for...of + await, then the reduce-based promise-chain equivalent — and contrast both with parallel Promise.all.

10 min read
JavaScript
Interview
Implementation
Async

TABLE OF CONTENTS

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:

  1. runSeries(tasks) — array of zero-arg async functions, collect all results
  2. mapSeries(arr, fn) — like Array.map but async and sequential (mirrors mapAsync)
  3. 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

  • await inside a loop — knowing that await inside for...of is 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

TimeSpace
runSeries(N tasks)O(T₁ + T₂ + ... + Tₙ) — sum of all task timesO(N) for results
mapAsync(N tasks)O(max(T₁, T₂, ..., Tₙ)) — longest taskO(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 await inside for...of explicitly — some candidates write tasks.forEach(async t => await t()) which doesn't serialize — forEach doesn't await the returned promises. Use for...of.
  • Name the reduce pattern — "This is a promise chain — each .then() gates the next task. The reduce just builds the chain programmatically."

Related Questions


Let's Connect

© 2026 Naveen Karthik // Built with React & MUI