All questions

Map Async Limit

Premium

Map Async Limit

Implement mapAsyncLimit(items, limit, asyncMapper) — the bounded-concurrency cousin of mapAsync. The plain parallel version fires every mapper at once; with 1000 items that's 1000 concurrent requests, which exhausts sockets, trips API rate limits, and saturates the browser's connections-per-host cap. mapAsyncLimit adds a knob: process the array with at most limit mappers in flight at any moment, but still resolve with the results in input order — and still fast-fail on the first rejection.

Signature

// Returns a Promise that fulfils with results[] in input order.
// At most `limit` mappers run concurrently. Rejects on first failure.
function mapAsyncLimit<T, U>(
  items: Iterable<T>,
  limit: number,
  asyncMapper: (item: T, index: number) => Promise<U> | U,
): Promise<U[]>;

Examples

// Basic bounded fetch. limit=2 means at any moment, at most two
// mappers are in flight. The remaining items wait until a slot opens.
const ids = [1, 2, 3, 4, 5];
const users = await mapAsyncLimit(ids, 2, (id) =>
  fetch(`/api/users/${id}`).then((r) => r.json()),
);
// users === [user1, user2, user3, user4, user5]
// Fast-fail. The first rejection wins; later successful mappers
// have their results discarded, and in-flight ones aren't waited on
// for a new claim (though they can't be cancelled).
await mapAsyncLimit([1, 2, 3, 4], 2, async (n) => {
  if (n === 2) throw new Error('boom');
  return n * 10;
}); // throws Error('boom')
// limit greater than items.length behaves like mapAsync — every
// mapper kicks off in the same tick; wall-clock = max(per-item time).
await mapAsyncLimit([10, 20, 30], 100, async (n) => n * 2);
// → [20, 40, 60]
// limit = 1 forces sequential execution. Wall-clock = sum of times,
// not max. Useful for rate-limited APIs (one in flight at a time).
await mapAsyncLimit([1, 2, 3], 1, async (n) => n);
// → [1, 2, 3]

Notes

  • At most limit in flight. Not "process in chunks of limit" — that idles workers. A free worker should grab the next item the moment it finishes, even if other workers are still busy.
  • Input order, not completion order. results[i] always holds the value of asyncMapper(items[i], i) — even if mapper i finishes last.
  • Mapper receives (item, index). Same shape as Array.prototype.map. The third argument is not required.
  • Fast-fail on rejection. Match Promise.all semantics: the outer promise rejects the moment any mapper rejects. Don't wait for in-flight workers to finish before rejecting; just stop claiming new items.
  • limit <= 0 throws a RangeError. Zero workers means no progress; that's a programmer mistake, not a runtime condition to silently accept.
  • limit = Infinity is allowed and behaves like the unbounded mapAsync — every mapper runs in parallel.
  • Accepts any iterable. Arrays, generators, Sets. The function snapshots the iterable into an array at the start (generators are single-pass; we need indexable access for results[i]).
  • Synchronous throws in the mapper are rejections. Same rule as mapAsync.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium