17. Async Task Pool — Run Promises with a Concurrency Limit

hard

This is the senior async question. It separates people who know Promise.all from people who understand concurrency control — and it shows up whenever you must avoid hammering an API or the file system.

The idea

Promise.all starts everything at once. Here you only want limit tasks running concurrently:

  1. Kick off tasks as you iterate, storing each promise by index so the final order matches the input.
  2. Keep the running promises in a Set and let each one remove itself on settle.
  3. When the set is full, await Promise.race(inFlight) — that resolves as soon as one finishes, freeing a slot.

The traps

  • Order — collect by index; Promise.all(results) returns them in input order regardless of finish time.
  • Self-cleanup — attach .then(remove, remove) so both success and failure free the slot.
  • One failurePromise.all rejects on the first failure; mention allSettled if the task should be resilient.

Follow-ups

  • "Stream results as they finish instead of waiting for all?" → make it an async generator.
  • "Add AbortSignal?" → check signal.aborted before launching each task.

What to practice

Build the pool, then verify with the second test case that the in-flight count never exceeds the limit.

More questions

index.js
async function asyncPool(limit, items, iteratorFn) {
  // run iteratorFn(item) over all items, at most `limit` at a time,
  // resolving to results in input order
}

const sleep = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
asyncPool(2, [100, 50, 30], (ms) => sleep(ms, ms)).then(console.log); // [100, 50, 30]

Tests

Test Code

Enter JavaScript that runs after your solution. It should return a value or a Promise.