Implement mapAsync() and mapAsyncLimit()

mapAsync runs an async mapping function over an array. mapAsyncLimit adds a concurrency cap — never more than N requests in flight at once. The second is the real interview question.

12 min read
JavaScript
Interview
Implementation
Async
Concurrency

TABLE OF CONTENTS

mapAsync runs an async function over every element in an array. The real interview question is mapAsyncLimit — do the same thing but never have more than N operations running at once. It's the concurrency pool pattern.

Related deep-dive: Async #8 — Concurrency Control & Retry Patterns


What is mapAsync()?

mapAsync(array, asyncFn) is the async equivalent of Array.prototype.map. It applies an async function to every element and returns a Promise that resolves with the results array, preserving input order. The simple version is Promise.all(array.map(asyncFn)) — but the real interview question is mapAsyncLimit.

mapAsyncLimit(array, limit, asyncFn) adds concurrency control: it ensures at most limit async operations run simultaneously. This is the concurrency pool pattern — you have N items to process but only K workers, and you need to keep all K workers saturated until the queue is drained.

This is a fundamental async pattern that appears everywhere:

  • API rate limiting — never send more than 3 concurrent requests to a rate-limited endpoint
  • File processing — process a directory of 10,000 files with at most 50 concurrent fs.readFile calls to avoid exhausting file descriptors
  • Browser resource limits — browsers cap concurrent requests to the same origin (typically 6); mapAsyncLimit lets you control your own concurrency below that
  • Database connection pools — run queries with concurrency matching your pool size to maximize throughput without queuing

The implementation is more involved than mapAsync. You need an active count tracker, a result array with index-based assignment, and a dispatch loop that launches new work as slots free up. The standard approach is either a manual loop with .then() chaining or an async generator that pulls from a queue.


The Problem

"Implement mapAsync(array, asyncFn) that applies an async function to every element and returns a promise resolving with the results array."

"Now implement mapAsyncLimit(array, asyncFn, limit) — same thing, but at most limit operations can run concurrently."


Thought Process

Version 1 is trivial: Promise.all(array.map(asyncFn)). Done.

Version 2 is about the concurrency pool pattern:

  1. Maintain a count of in-flight operations
  2. Maintain a queue of pending items
  3. When an operation completes, decrement the count and start the next one from the queue
  4. Resolve the outer promise when all items have been processed

Alternative approaches:

  • Chunk-based: split the array into chunks of size limit, process each chunk sequentially. Flaw: a slow item in a chunk holds up the entire next chunk.
  • Running-count: start limit items immediately; as each finishes, start the next. This is the correct approach.

Step 1 — mapAsync (Unlimited Concurrency)

Loading editor...


Step 2 — mapAsyncLimit (Running-Count Approach)

Loading editor...


Step 3 — Error Handling

The interviewer asks: "What if one of the async functions rejects?"

Loading editor...


Step 4 — Edge Cases

limit > array.length: All items start immediately — effectively mapAsync.

limit = 1: Sequential execution. Each item waits for the previous one to finish.

limit = 0: No items ever start; the promise never settles. Guard against this at the top.

Empty array: Return resolved promise with [].

Non-promise return: Promise.resolve(asyncFn(...)) handles sync returns.


Full Solution

Loading editor...


What Interviewers Are Testing

  • Concurrency control — capping in-flight operations with a running count
  • Promise wrappingPromise.resolve() to handle sync and async return values
  • Index tracking — preserving result order despite out-of-order completion
  • Error propagation — rejecting the outer promise while allowing in-flight operations to finish (or short-circuiting)
  • Queue vs chunk distinction — knowing why a dynamic queue beats fixed chunks

Complexity

TimeSpace
mapAsyncO(N) — all run in parallelO(N) — results array
mapAsyncLimitO(N × T/limit) — T is avg task timeO(N) — results + O(limit) in-flight

Interview Tips

  • Write mapAsync in one line first — "Promise.all(array.map(asyncFn)) is the unlimited version." Then explain why you need limits.
  • Name the "running count" pattern — "This is the concurrency pool pattern — maintain a count of in-flight operations, and start a new one every time one finishes."
  • Explain why chunks are wrong — "Splitting into fixed chunks means a slow item blocks the next chunk. The pool approach starts the next item as soon as any slot frees up."
  • Ask about error behavior — "On rejection, should I wait for in-flight operations to finish, or reject immediately?" Shows awareness of the design decision.

Related Questions


Let's Connect

© 2026 Naveen Karthik // Built with React & MUI