Promise.all coordinates promises that have already been started; it does not impose a limit. Mapping ten thousand inputs to requests can start ten thousand operations before awaiting any. A concurrency limit must control when the mapper is called.
One simple implementation processes batches. Each batch starts at most limit jobs, waits for all of them, then starts the next batch. This is easy to reason about but leaves capacity unused when one job in a batch is much slower than its peers. A worker-pool design refills an available slot immediately; Day 7 supplies that complete implementation.
Concurrency overlaps waiting. It does not make synchronous CPU work run in parallel on the JavaScript thread. The limit should be chosen for the resource being protected, not from a claim that a larger number is always faster. The active-call count excludes jobs that have already completed.
Predict, run, and explain
Write the output and trace the important state changes before checking it. Then run the program. Each editor is independent. Async examples end with main() so this editor waits for their work.
async function main() {
let active = 0;
let peak = 0;
async function task(value) {
active++;
peak = Math.max(peak, active);
await Promise.resolve();
active--;
return value * 2;
}
const results = [];
for (let start = 0; start < 5; start += 2) {
const group = [1, 2, 3, 4, 5].slice(start, start + 2);
results.push(...(await Promise.all(group.map(task))));
}
console.log(results.join(","));
console.log(peak);
}
main();Check your prediction
2,4,6,8,10
2 Measure active work rather than counting promises
Concurrency bounds are about operations that have started and not settled. Merely creating an array with two elements does not prove at most two callbacks are active throughout a larger run. Observe starts, completions and peak activity through the mapper boundary.
A batch leaves capacity idle while its slowest member finishes. A worker loop can refill a free slot immediately. Both can preserve result order by reserving positions before awaiting; neither makes synchronous CPU work parallel on the same JavaScript thread.
async function main() {
let active = 0;
let peak = 0;
async function work(n) {
active++;
peak = Math.max(peak, active);
await Promise.resolve();
active--;
return n;
}
await work(1);
await work(2);
console.log(peak);
peak = 0;
await Promise.all([work(1), work(2)]);
console.log(peak);
}
main();Check your prediction
1
2 The same mapper is used in both schedules. Sequential await prevents overlap; calling both before awaiting permits it. Peak counts active invocations, not array length after completion.
async function main() {
const results = [];
const first = Promise.resolve("A");
const second = Promise.resolve("B");
await Promise.all([
second.then(value => {
results[1] = value;
}),
first.then(value => {
results[0] = value;
})
]);
console.log(results.join(","));
}
main();Check your prediction
A,B Each callback writes to the input’s reserved position, independent of its registration order. A later controlled schedule can reverse completion without changing the output contract.
Try the rule on new inputs
Compare limits one and two using active and peak counters. Then hold one callback on a deferred promise and identify the event that proves another worker slot was refilled before releasing it.
Your turn
Bounded Concurrency: practise
Write batchMap(values, mapper, limit) that validates a positive integer limit and processes one group at a time with Promise.all. For [1,2,3] and an async doubler at limit 2, print 2,4,6.
// Use async main() and call it last; define the batch helper inside main.Hint 1: the idea
Do not invoke the mapper for the next group before the current group settles.
Hint 2: a first step
Slice the input from start to start + limit, then await that group.
Show one solution
async function main() {
async function batchMap(values, mapper, limit) {
if (!Number.isInteger(limit) || limit < 1) {
throw new RangeError("Invalid limit");
}
const result = [];
for (let start = 0; start < values.length; start += limit) {
result.push(
...(await Promise.all(values.slice(start, start + limit).map(mapper)))
);
}
return result;
}
console.log(
(await batchMap([1, 2, 3], async value => value * 2, 2)).join(",")
);
}
main();2,4,6Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?
From batches to worker slots
Before Day 7, learn the core loop without failure or cancellation policies. Here a worker is an async function, not a Web Worker or another thread. Two calls share one next counter. Each reserves its index synchronously, then pauses at await. When a call resumes, it can reserve another item without waiting for the other slot.
async function main() {
async function pool(values, mapper, limit) {
let next = 0;
const results = [];
async function worker() {
while (next < values.length) {
const index = next++;
results[index] = await mapper(values[index]);
}
}
const workers = [];
for (let slot = 0; slot < limit; slot++) {
workers.push(worker());
}
await Promise.all(workers);
return results;
}
console.log((await pool([3, 1, 2], async n => n * 10, 2)).join(","));
}
main();Check your prediction
30,10,20 Trace which part runs before the first await. Reserving the index afterward would let two calls choose the same item. Assigning by index preserves input order even when completion order differs.
The capstone writes the worker-start loop as Array.from({ length: limit }, () => worker()): the first argument requests that many positions, and the callback starts one worker per position. It also replaces array indexing with the iterator protocol from Day 2. This small version assumes valid inputs and successful callbacks. If one rejects, Promise.all rejects early while other work may continue; Day 7 adds the explicit stop-and-settle policy. Day 5 teaches how to test these schedules without timing guesses.
Change it and test it
Test empty input and limit 1. Add an active counter and assert its peak stays within the limit. Explain the slow-batch limitation and why this toy does not yet promise peer cancellation or waiting for peers before rejecting.
Completion is saved in this browser only. It does not measure mastery.