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.
// 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[]>;
// 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]
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.results[i] always holds the value of asyncMapper(items[i], i) — even if mapper i finishes last.(item, index). Same shape as Array.prototype.map. The third argument is not required.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.Sets. The function snapshots the iterable into an array at the start (generators are single-pass; we need indexable access for results[i]).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.