Promise combinators
Learn how JavaScript combines many promises with Promise.all, allSettled, race, any, withResolvers, Promise.try, and the proposed keyed helpers.
- 01Wait for the right amountUse all, allSettled, race, and any when you need every result, every report, the first settlement, or the first success.
- 02Handle edge cases honestlyPredict empty iterables, input order, fail-fast rejection, AggregateError, and non-promise values.
- 03Recognize newer toolsFeature-detect withResolvers, Promise.try, and the Stage 3 allKeyed proposal while using safe fallbacks.
Many promises, one plan
A single promise is a value for work that finishes later. Real pages rarely wait for just one thing. A dashboard may need the user, feature flags, notifications, and chart data. A file uploader may start many uploads. A CDN loader may ask three mirrors and keep the first good answer.
Promise combinators are static methods that take an iterable of promises or values and return one new promise that describes the group. This lesson builds on Promises and Microtasks in depth. The previous lessons by title, Promise chaining and Error handling with promises, explain how values and rejections travel once the group promise settles.
Use Promise.all for every success, Promise.allSettled for every report, Promise.race for the first settlement, and Promise.any for the first fulfillment.
Imagine four friends ordered together. The table is served only when every dish is ready. If the kitchen drops one dish, the waiter comes back immediately with the bad news. But the soup and dessert do not magically vanish from the kitchen; they keep cooking.
- In real life: Every dish reaches the table
- In JavaScript: Every input fulfills, so
Promise.allfulfills - In real life: The waiter keeps dish positions straight
- In JavaScript: The result array follows input order
- In real life: One dish is dropped
- In JavaScript: The combined promise rejects with the first rejection reason
- In real life: Other dishes keep cooking
- In JavaScript: Fail-fast does not cancel the other promises
Where the analogy stops: A restaurant might tell the kitchen to stop. Promise.all has no built-in cancellation; timers, requests, and other work keep running unless you add your own cancellation signal.
That is the pattern for the whole lesson: each method answers a different group question. Do you need every value, every outcome, the first outcome, or the first success?
Promise.all & allSettled
INTERACTIVEPromise.all(iterable) accepts any iterable. Each item can be a promise, a thenable, or a plain value. Plain values count as already fulfilled. If the iterable is empty, Promise.all([]) fulfills with an empty array.
The success result is an array in input order, not completion order. That matters when one promise is slow but appears first in the array. Try the kitchen race track, then step through the fail-fast replay.
const tasks = [ task("A", 800, true), task("B", 450, true), task("C", 1100, false),]; Promise.all(tasks); // every task must fulfillPromise.allSettled(tasks); // report every resultPromise.race(tasks); // first settlement winsPromise.any(tasks); // first fulfillment winsPress Run to start real promises.
- No settlements yet.
Promise.all waits for every fulfillment, but rejects as soon as the first task rejects. Notice that the other timers still finish.
Choose which task fails. Predict whether Promise.all logs an array or the first rejection reason.
script
function cook(label, ms, shouldFail) { return new Promise((resolve, reject) => { setTimeout(() => { if (shouldFail) reject(label + " dropped"); else resolve(label + " ready"); }, ms); });} const orders = [ cook("B", 10, true), cook("C", 20, false),]; Promise.all(orders).then( (values) => console.log(values.join(" | ")), (reason) => console.log("failed: " + reason),);Promise.allSettled(iterable) waits for everything and always fulfills because of the inputs. Each result is an object: either { status: "fulfilled", value } or { status: "rejected", reason }. Use it when failures are data you still need to show, such as upload reports or test results.
A teacher does not stop collecting exams because one student failed. The report is useful only after every paper is in. That is allSettled: no early exit, no missing rows.
- In real life: Every paper is collected
- In JavaScript: Every input settles before the combined promise fulfills
- In real life: Some exams pass, some fail
- In JavaScript: Each result has a fulfilled or rejected status
- In real life: The teacher announces the full class report
- In JavaScript: You receive an array of status objects
Where the analogy stops: The teacher might grade papers later. Promise.allSettled does not grade or fix failures; it only reports each settlement.
Promise.race & Promise.any
SORT ITPromise.race(iterable) settles as soon as the first input settles. If the fastest promise fulfills, the race fulfills. If the fastest promise rejects, the race rejects. An empty race, Promise.race([]), stays pending forever because there is no first settlement.
Promise.any(iterable) waits for the first fulfillment. Rejections are ignored until every input has rejected. If that happens, the returned promise rejects with an AggregateError, and its .errors array is in input order. Promise.any([]) rejects immediately with an AggregateError because there can never be a fulfillment.
In a horse race, the first event decides the headline: a horse wins or a favorite falls. In job applications, a single yes is enough; you give up only when everyone says no. That is the difference between race and any.
- In real life: First horse crosses the line or falls
- In JavaScript:
Promise.raceuses the first settlement, fulfilled or rejected - In real life: One job offer is enough
- In JavaScript:
Promise.anyfulfills with the first success - In real life: Every company says no
- In JavaScript:
Promise.anyrejects withAggregateError
Where the analogy stops: A race usually has one winner and ends for everyone. Promise.race does not cancel the slower promises; they can still settle later.
- Load profile, permissions, and settings before showing a dashboard
- Upload 8 photos and show which ones succeeded or failed
- Use a timeout promise to stop waiting after 3 seconds
- Try three CDNs and use the first successful response
- Collect every exam result before announcing the class report
- Show whichever connection attempt settles first, success or failure
- Wait for soup, salad, and dessert before serving a table
- Send applications and celebrate the first yes
Choose the group method that matches each real scenario.
await Promise.all(["plain", Promise.resolve("promise")]);await Promise.all([]);Promise.race([]); // stays pending foreverawait Promise.any([]); // rejects with AggregateErrorPromise.withResolvers
NEWER APISometimes the code that creates a promise is not the same code that knows when to settle it. Promise.withResolvers() returns an object with promise, resolve, and reject. It is a Baseline 2024/newer-environment feature; Node 22 includes it, but feature detection is still healthy when you support older browsers.
You get the token, while the counter keeps the buttons that mark it ready or sold out. withResolvers lets a careful piece of code, such as an event listener, use those buttons.
- In real life: You hold an order token
- In JavaScript: The
promisesomeone can await - In real life: The counter marks it ready
- In JavaScript: The
resolvefunction - In real life: The counter marks it sold out
- In JavaScript: The
rejectfunction
Where the analogy stops: Handing out resolve and reject is powerful. Keep them private unless another part of your code truly owns the event that settles the promise.
function waitForClick(button) { const { promise, resolve, reject } = Promise.withResolvers(); button.addEventListener("click", () => { resolve("button clicked"); }, { once: true }); return { promise, cancel: () => reject("cancelled") };}checking…
- Create an order token, then settle it from outside.
withResolvers separates creating the promise from deciding who will settle it.
A realistic use is waitForClick(button): create a promise, resolve it when a real click happens, and optionally reject it if the wait is canceled.
Promise.try
FEATURE DETECTPromise.try(fn, ...args) is a safety net for code that might fail before it returns a promise. It calls fn now, wraps the return value in a promise, and turns a synchronous throw into a rejection. It is an ES2025/newer-browser feature and is not in Node 22, so this lesson uses a small fallback in tests.
Without a safety net, you need one tool for falling now and another for falling later. Promise.try lets both mistakes become promise rejections.
- In real life: Trip immediately on stage
- In JavaScript: A synchronous
throwinsidefn - In real life: Slip later on the rope
- In JavaScript: A promise returned by
fnlater rejects - In real life: Same net catches both
- In JavaScript: One
.catchhandles either failure
Where the analogy stops: Promise.try does not make the function's work synchronous. Promise handlers still run later through the promise job queue.
function promiseTry(fn, ...args) { if (typeof Promise.try === "function") { return Promise.try(fn, ...args); } return new Promise((resolve) => resolve(fn(...args)));} promiseTry(readConfig) .then((value) => console.log(value)) .catch((error) => console.log(error.message));- Choose a case, then compare starts.
Promise.try calls the function right away, but turns a synchronous throw into a rejected promise so one catch path can handle both kinds of failure. Promise.resolve().then(fn) starts fn in a later microtask.
Compare it with Promise.resolve().then(fn). That pattern also turns a thrown error into a rejection, but it starts fn later in a microtask. Promise.try(fn) starts fn immediately.
Await dictionaries: Promise.allKeyed
PROPOSALArrays are easy to mix up when each position means something different. The TC39 proposal informally known as “Await dictionary of Promises” adds Promise.allKeyed and Promise.allSettledKeyed. It is Stage 3 and no engine ships it yet. The proposal awaits all own enumerable keys, including symbols. The helper below is intentionally smaller and uses Object.keys, so it handles string keys.
A lunchbox with labels is harder to mess up than a row of identical bowls. Keyed promise helpers keep the names next to the results.
- In real life: Each compartment has a label
- In JavaScript: Each property name stays attached to its value
- In real life: Rice and fruit don't swap places
- In JavaScript: You read
result.user, not position 0 - In real life: A broken compartment still has a label
- In JavaScript:
allSettledKeyedreports status by key
Where the analogy stops: The local helper uses Object.keys for clarity. The Stage 3 proposal is broader and includes own enumerable symbol keys too.
const [alerts, user] = await Promise.all([ fetchUser(), fetchAlerts(),]);// Oops: alerts contains the user, and user contains the alerts.async function allKeyed(object) { const keys = Object.keys(object); const values = await Promise.all(keys.map((key) => object[key])); return Object.fromEntries(keys.map((key, index) => [key, values[index]]));} const dashboard = await allKeyed({ user: fetchUser(), stats: fetchStats(), alerts: fetchAlerts(),});async function allSettledKeyed(object) { const keys = Object.keys(object); const results = await Promise.allSettled(keys.map((key) => object[key])); return Object.fromEntries(keys.map((key, index) => [key, results[index]]));}checking…
- Run the helper to keep labels attached.
The Stage 3 proposal awaits a dictionary and keeps property names. This helper is smaller: it uses Object.keys, so it covers string keys only and skips symbols.
async function allKeyed(object) { const keys = Object.keys(object); const values = await Promise.all(keys.map((key) => object[key])); return Object.fromEntries(keys.map((key, index) => [key, values[index]]));} const dashboard = await allKeyed({ user: fetchUser(), stats: fetchStats(), alerts: fetchAlerts(),});Where you’ll use combinators
Combinators make async intent readable. A loader using Promise.all says “this page needs every piece.” A report using allSettled says “show every outcome.” A CDN loader using any says “first success wins.” A timeout preview using race says “the first settlement decides.”
async function loadDashboard() { const [user, widgets, flags] = await Promise.all([ loadUser(), loadWidgets(), loadFeatureFlags(), ]); const uploads = await Promise.allSettled(files.map(uploadFile)); const fastest = await Promise.any(cdnMirrors.map(loadFromMirror)); return { user, widgets, flags, uploads, fastest };}Racing a timeout promise is common, but real production cancellation usually adds AbortController so the slower request can be stopped too. The full pattern appears in the Async patterns lesson later in the course.
const timeout = new Promise((_, reject) => { setTimeout(() => reject(new Error("too slow")), 3000);}); const data = await Promise.race([loadData(), timeout]);Common misconceptions
| Method | Fulfills when | Rejects when | Best for |
|---|---|---|---|
Promise.all | Every input fulfills; values are in input order | The first input rejects | Required parallel data |
Promise.allSettled | Every input settles with status objects | Never because of an input | Complete reports |
Promise.race | The first settlement is fulfillment | The first settlement is rejection | First response or timeout previews |
Promise.any | The first input fulfills | Every input rejects, with AggregateError | Fallbacks where one success is enough |
- “Promise.all cancels the rest.” It does not. It rejects early, but other work keeps running unless you cancel it yourself.
- “Results come back in finish order.”
allandallSettledpreserve input order. - “allSettled gives me values directly.” It gives status objects. Check
statusbefore readingvalueorreason. - “race means first success.”
racemeans first settlement. Useanyfor first success. - “New APIs are everywhere.” Feature-detect
withResolvers,Promise.try, and proposals such asallKeyed.
Practice exercises
5 EXERCISESRun the code in your head. What one line does it print?
const slow = new Promise((resolve) => setTimeout(() => resolve("slow"), 20));
const fast = Promise.resolve("fast");
Promise.all([slow, fast, "plain"]).then((values) => console.log(values.join(",")));const slow = new Promise((resolve) => setTimeout(() => resolve("slow"), 20));
const fast = Promise.resolve("fast");
Promise.all([slow, fast, "plain"]).then((values) => console.log(values.join(",")));slow finishes after fast, but it is the first input, so the fulfilled array is ["slow", "fast", "plain"].
Which value reaches the then handler?
Promise.any([
Promise.reject("no A"),
new Promise((resolve) => setTimeout(() => resolve("yes B"), 5)),
Promise.resolve("yes C"),
]).then((value) => console.log(value));Promise.any([
Promise.reject("no A"),
new Promise((resolve) => setTimeout(() => resolve("yes B"), 5)),
Promise.resolve("yes C"),
]).then((value) => console.log(value));The first input rejects, the second fulfills after a timer, and the third is already fulfilled, so Promise.any prints yes C.
What status string prints?
Promise.allSettled([Promise.resolve("ok"), Promise.reject("bad")])
.then((results) => console.log(results.map((item) => item.status).join("/")));Promise.allSettled([Promise.resolve("ok"), Promise.reject("bad")])
.then((results) => console.log(results.map((item) => item.status).join("/")));The first input fulfills and the second rejects, but allSettled fulfills with both statuses: fulfilled/rejected.
Fill in or study the helper, then predict its output.
async function allKeyed(object) {
const keys = Object.keys(object);
const values = await Promise.all(keys.map((key) => object[key]));
return Object.fromEntries(keys.map((key, index) => [key, values[index]]));
}
allKeyed({ user: Promise.resolve("Ada"), count: 3 })
.then((result) => console.log(result.user + " " + result.count));async function allKeyed(object) {
const keys = Object.keys(object);
const values = await Promise.all(keys.map((key) => object[key]));
return Object.fromEntries(keys.map((key, index) => [key, values[index]]));
}
allKeyed({ user: Promise.resolve("Ada"), count: 3 })
.then((result) => console.log(result.user + " " + result.count));The helper awaits values with Promise.all, then rebuilds an object with the original keys, so result.user and result.count stay labeled.
What does the fallback catch print?
function promiseTry(fn) {
if (typeof Promise.try === "function") return Promise.try(fn);
return new Promise((resolve) => resolve(fn()));
}
promiseTry(() => { throw new Error("sync boom"); })
.catch((error) => console.log(error.message));function promiseTry(fn) {
if (typeof Promise.try === "function") return Promise.try(fn);
return new Promise((resolve) => resolve(fn()));
}
promiseTry(() => { throw new Error("sync boom"); })
.catch((error) => console.log(error.message));Because the callback throws inside the Promise constructor's executor, the returned promise rejects and the catch logs sync boom.
Check your understanding
8 QUESTIONSQuestion 1 of 8What does Promise.all do when one input rejects?
Choose an answer to see the explanation.
Question 2 of 8What does this print?
Read the code, then predictPromise.all([Promise.resolve("A"), "B"]) .then((values) => console.log(values.join("-")));Choose an answer to see the explanation.
Question 3 of 8Which combinator never rejects because one input rejected?
Choose an answer to see the explanation.
Question 4 of 8What happens with Promise.race([])?
Choose an answer to see the explanation.
Question 5 of 8What does Promise.any reject with when every input rejects?
Choose an answer to see the explanation.
Question 6 of 8What is Promise.withResolvers useful for?
Choose an answer to see the explanation.
Question 7 of 8Why feature-detect Promise.try in this lesson?
Choose an answer to see the explanation.
Question 8 of 8What is true about the allKeyed proposal?
Choose an answer to see the explanation.
Key takeaways
Promise.allis for “all must succeed.” It preserves input order and rejects with the first rejection reason.Promise.allSettledis for reports. It waits for every input and gives status objects.Promise.raceis first settlement;Promise.anyis first fulfillment orAggregateError.withResolversandPromise.tryare newer APIs, so feature-detect and use fallbacks when needed.allKeyedis a Stage 3 proposal; a smallObject.keyshelper can teach the pattern today.
Final definition: Promise combinators turn many promised outcomes into one promise whose rules match the way your feature wants to wait.
Up next: Promisification & promise-based APIs.