Prompt and context
This coding question tests concurrency scheduling rather than putting every promise into Promise.all. The candidate must define a contract for throughput, ordering, error propagation, cancellation, and empty input, then show that the limit is never exceeded and no item is consumed twice.
What the interviewer evaluates
- Whether fixed workers or an equivalent scheduler keep at most N active tasks.
- Whether results are written by input index so completion order cannot reorder output.
- Whether fail-fast and collect-errors semantics are explicit, including already-running tasks.
- Whether invalid N, empty input, synchronous throws, cancellation, and non-Promise mapper values work.
Clarifying questions to ask
Confirm whether tasks may continue after cancellation, whether fail-fast waits for cleanup, whether partial results are returned, and whether mapper accepts an AbortSignal. Ask about retries, dynamic concurrency, and preserving original error object identity.
30-second answer framework
I would create no more than N workers sharing a next-index counter. Each worker claims an index, wraps synchronous returns and throws with Promise.resolve, and writes by index. Fail-fast propagates the first observed error and stops new dispatch; collect mode records each status. Cancellation stops new claims and passes the signal to mapper. Work is O(items), result and status space is O(items), and active work is bounded by O(N).
Step-by-step deep dive
1. Define result and error contracts
Results keep input order. Collect mode returns fulfilled or rejected status with the original value or reason; undefined cannot be a success marker. Fail-fast returns the first observed error, while noting that JavaScript cannot forcibly kill already-started promises. A mapper that accepts a signal can cooperate with cancellation.
2. Use fixed workers and index allocation
Keep the next index in shared state. Each worker loops until the index is out of range, cancellation is set, or fail-fast stops dispatch. Increment before starting a task so one item can be claimed only once. Use min(N, items.length) workers to avoid idle promises.
3. Handle synchronous throws and completion order
Catch a synchronous mapper throw and normalize both values and promises with Promise.resolve. Write success or failure to the original index; never push from completion callbacks. As soon as one worker finishes, it claims the next index, preserving throughput without increasing active work.
4. Propagate cancellation and stop dispatch
If the signal is already aborted, reject or return the documented cancellation error before starting work. Running tasks receive the same signal. The scheduler waits for workers to exit before settling its main promise, so callers do not observe completion while a background loop keeps dispatching. Cancellation and business errors need distinguishable causes.
5. Verify edges and complexity
Test an empty array, N equal to zero, N larger than input, synchronous values and throws, varied delays, first failure, multiple failures, and mid-flight cancellation. Use an active counter to assert it never exceeds N and a hook to confirm each index runs once. Mapper calls are O(items), result space is O(items), and scheduler concurrency is O(N).
Strong sample answer
I validate that N is a positive integer, create min(N, items.length) workers, and share a next-index counter. A worker claims an index, calls mapper through Promise.resolve so synchronous values and throws follow the same path, and writes the result by index. Fail-fast sets a stop-dispatch flag and propagates the first error; collect mode keeps each status. Cancellation prevents new claims and passes the signal to active mappers, then waits for workers to exit. Tests cover reordered delays, synchronous errors, empty input, N boundaries, and cancellation, with an assertion that active work never exceeds N.
Common mistakes
- Calling
Promise.all(items.map(mapper))and starting every task at once. - Pushing results so output follows completion order instead of input order.
- Catching only promise rejections and missing synchronous mapper throws.
- Continuing to dispatch after fail-fast, or assuming reject automatically cancels running work.
- Treating cancellation as an ordinary business error so callers cannot choose retry behavior.
- Skipping validation for N, empty input, and ordinary non-Promise mapper values.
Follow-up questions and answers
Why not recursively start the next item?
Recursion can express a serial chain, but it must also maintain concurrency, errors, and cancellation. Fixed workers make the active-task proof clearer and avoid deep recursion or duplicate dispatch.
What happens to tasks already running during fail-fast?
JavaScript promises have no general force-termination primitive. Stop new dispatch and call abort when the mapper cooperates with AbortSignal; otherwise let tasks finish without writing to an externally settled result.
How should collect mode represent errors?
Return a per-item status object distinguishing fulfilled and rejected while preserving the original value or reason. A sentinel is unsafe because a successful value may itself be undefined or null.
How would you adjust concurrency dynamically?
Track the configured maximum separately from active work and add or stop claims at safe boundaries. Dynamic adjustment adds proof and test cost, so use a fixed limit unless the requirement explicitly needs adaptation.