JAVASCRIPT / PROMISES AND ASYNC PATTERNS
Promise.all for parallel independent work
Run independent async work at the same time by starting each call first, awaiting a single Promise.all, and handling its ordered results and fail-fast errors.
What you will learn
- Start every independent call before awaiting, then await one Promise.all
- Destructure results by input position, not by which promise finished first
- Expect elapsed time near the slowest member, not the sum of all members
- Wrap the await in try/catch: the first rejection rejects all and cancels nothing
Understanding Promise.all for parallel independent work
Promise.all is an aggregator, not a scheduler. By the time you hand it an array, every operation in that array is already running, because a promise is a handle on work that has started: calling getUser() fires the request whether or not anyone is awaiting the result. What Promise.all adds is a single promise to await and a single results array whose entries sit at the same index as the promise that produced them, so you can destructure [user, stats] without tracking who finished first.
That indexing is also why elapsed time collapses to the slowest member instead of the sum. A sequential 'await a(); await b();' does not merely wait twice, it delays starting b until a has settled, because the second call is a statement placed after a suspension point. Timers and network requests are serviced outside the JS thread, so many can be outstanding at once, and Promise.all lets you wait for all of them in one place instead of inventing an artificial dependency between them.
The price is all-or-nothing settlement. The aggregate rejects the instant the first member rejects, carrying that member's reason and no results array, and it does not cancel the others, because promises have no cancel operation: their timers keep ticking and their side effects still land. Whatever the successful members produced is discarded, and a later rejection is swallowed since only the first reason is reported, which makes Promise.all the right tool only when a partial result is worthless to you.
const delay = (ms, value) =>
new Promise(resolve => setTimeout(() => resolve(value), ms));
async function loadDashboard() {
const start = Date.now();
// Calling delay() starts each timer right here; nothing is awaited yet.
const userP = delay(300, 'user:ada');
const statsP = delay(200, 'stats:42');
const feedP = delay(100, 'feed:3-items');
// One await for three in-flight operations; results arrive by position.
const [user, stats, feed] = await Promise.all([userP, statsP, feedP]);
console.log(user, stats, feed);
console.log('elapsed:', Math.floor((Date.now() - start) / 100) * 100 + 'ms (slowest task, not the sum)');
}
loadDashboard();Promise.all does not start anything in parallel; it waits on promises that are already running and hands back their values in input order, rejecting as soon as one fails.
Worked examples
Serial loop against a mapped Promise.all
Shows that the same three jobs cost 300 ms when awaited one at a time and about 100 ms when started together.
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const job = n => delay(100).then(() => n * 2);
async function serial() {
const out = [];
for (const n of [1, 2, 3]) {
out.push(await job(n));
}
return out;
}
async function parallel() {
return Promise.all([1, 2, 3].map(n => job(n)));
}
(async () => {
let t = Date.now();
const a = await serial();
console.log('serial:', a.join(','), 'elapsed >= 300ms?', Date.now() - t >= 300);
t = Date.now();
const b = await parallel();
console.log('parallel:', b.join(','), 'elapsed < 200ms?', Date.now() - t < 200);
})();Example explained
Line 1[1, 2, 3].map(n => job(n)) calls job three times immediately, so all three 100 ms timers are already running before Promise.all sees them.
Line 2In serial(), 'await job(n)' suspends the loop body until that job settles, so the second timer only starts after the first one fired: 100 + 100 + 100.
Line 3Both versions log 2,4,6, because Promise.all keeps input order; switching to concurrency does not reshape your data.
Fail-fast does not cancel siblings
Demonstrates that the aggregate rejects at the first failure while the slower task keeps running to completion.
const delay = (ms, value) => new Promise(resolve => setTimeout(() => resolve(value), ms));
const failAfter = ms =>
new Promise((_, reject) => setTimeout(() => reject(new Error('boom')), ms));
const slow = delay(300, 'slow value').then(value => {
console.log('the 300ms task still finished:', value);
return value;
});
Promise.all([failAfter(100), slow])
.then(results => console.log('not reached', results))
.catch(err => console.log('Promise.all rejected at ~100ms:', err.message));Example explained
Line 1failAfter(100) rejects first, so the aggregate settles as rejected at about 100 ms and never builds a results array.
Line 2The 300 ms task was not stopped: its .then callback still runs and logs afterwards, because there is no cancellation in the promise model.
Line 3err.message is the single first reason; had the slow task also rejected, that later reason would have been dropped.
Promises, not function references
Shows what Promise.all does when you pass the functions instead of the promises they return.
const getUser = async () => 'ada';
const getStats = async () => 42;
(async () => {
const wrong = await Promise.all([getUser, getStats]);
console.log('wrong:', typeof wrong[0], typeof wrong[1]);
const right = await Promise.all([getUser(), getStats()]);
console.log('right:', right[0], right[1]);
})();Example explained
Line 1Promise.all wraps every non-thenable entry with Promise.resolve, so the two function objects pass straight through as fulfilled values.
Line 2Nothing was executed in the first call, so no error is raised and the bug surfaces later as a function where data was expected.
Line 3[getUser(), getStats()] passes the promises the calls returned, which is the only thing Promise.all can actually wait on.
Important notes
Promise.all creates no concurrency of its own, it only observes work that already started; two synchronous CPU-heavy functions still run one after the other on the single JS thread.
Mapping a huge input list fires every request at once, so batch the list when the target has rate or connection limits.
Common mistakes
Awaiting while building the array, as in Promise.all([await a(), await b()]): the two waits run back to back, so the total is the sum and Promise.all only wraps values that already finished.
Passing uncalled functions, as in Promise.all([getUser, getStats]): it fulfils on the next microtask with the function objects, no request is ever made, and nothing throws to warn you.
Treating the rejection as a stop signal: the siblings keep running, their writes and retries still happen, and both their results and their own errors are thrown away.
Try it yourself
Change, predict, then run
In a browser console, write wait(ms, label) that resolves with label after ms, then use Promise.all to run waits of 400, 250 and 100 ms and log both the joined results and the elapsed milliseconds. Then make the 250 ms one reject, and confirm the 400 ms one still logs after your catch has run.
Open the JavaScript workspaceCheck your understanding
getUser() and getOrders() each take about 500 ms and neither needs the other's result. Which snippet finishes in roughly 500 ms with both values available?
- const u = getUser(); const o = getOrders(); const [a, b] = await Promise.all([u, o]);
- const a = await getUser(); const b = await getOrders(); await Promise.all([a, b]);
- const [a, b] = await Promise.all([getUser, getOrders]);
- for (const f of [getUser, getOrders]) await Promise.all([f()]);
Show answer
In the first option both calls happen before any await, so the two 500 ms waits overlap and Promise.all simply reports when the later one settles. The second option is tempting because Promise.all still appears, but each await suspends until that call settles, so the waits run consecutively for about 1000 ms and Promise.all receives two plain values that add nothing. The third finishes almost instantly with two function objects because nothing was called, and the fourth awaits one call per loop iteration, which is serial again.