Promise combinators
Learn how JavaScript combines many promises with Promise.all, allSettled, race, any, withResolvers, Promise.try, and the proposed keyed helpers.
- 01Wait the right wayUse all, allSettled, race, and any when you need every value, a report for each promise, the first result, or the first success.
- 02Predict the small casesSay what happens with an empty array, a plain value, a failure in Promise.all, and an AggregateError. Know that values come back in input order.
- 03Use the newer methods safelyCheck that withResolvers, Promise.try, and the proposed allKeyed exist before you use them, and use a fallback when they don't.
Many promises, one plan
A promise is an object that stands for a result that isn't ready yet. You met it in the Promises lesson. Real pages often wait for many things at once:
- A dashboard may need the user, the settings, the messages, and the chart data.
- A photo uploader may send ten photos at the same time.
- A page may ask three servers for the same file and keep the first good answer.
Each promise ends in one of two ways. It fulfills: the work worked, and the promise has a value. Or it rejects: the work failed, and the promise has a reason. Both are called settling. A promise that hasn't settled yet is pending.
Promise combinators are methods that turn many promises into one promise. You call them on Promise itself, like Promise.all(...). You give them a list of promises, usually an array. They give back one new promise for the whole group. This lesson also uses ideas from Microtasks in depth, Promise chaining, and Error handling with promises.
Each method answers a different question about the group:
Promise.all: did every promise succeed? Give me all the values.Promise.allSettled: how did each promise end? Give me a report for each one.Promise.race: which promise finished first, success or failure?Promise.any: which promise succeeded first?
const user = Promise.resolve("Asha");const cartItems = Promise.resolve(3); Promise.all([user, cartItems]).then((values) => { console.log(values.join(" / "));});Line 1 makes a promise that fulfills with the name "Asha". Line 2 makes a promise that fulfills with the number 3, the number of items in a cart. Line 4 gives both promises to Promise.all in an array. When both are done, values is the array ["Asha", 3]. Line 5 joins the values and prints Asha / 3.
Four friends order dinner together. The waiter serves the table only when every dish is ready. If the kitchen drops one dish, the waiter comes back right away with the bad news. The soup and the dessert are still cooking in the kitchen.
- In real life: Every dish reaches the table
- In JavaScript: Every input fulfills, so
Promise.allfulfills - In real life: Each plate goes to the person who ordered it
- In JavaScript: The values come back in the order you passed the promises in
- In real life: The kitchen drops one dish
- In JavaScript: The group promise rejects with the reason from the first failure
- In real life: The other dishes keep cooking
- In JavaScript: The other promises keep running; nothing is stopped
Each method in this lesson answers one question. Do you need every value, a report for each one, the first result, or the first success?
Promise.all & allSettled
INTERACTIVEPromise.all takes an iterable: anything you can loop over with for...of, like an array. Each item can be:
- a promise,
- a thenable: any object with a
thenmethod, which JavaScript treats like a promise, - or a plain value, like
"plain". A plain value counts as a promise that has already fulfilled.
If the array is empty, Promise.all([]) fulfills right away with an empty array.
The values come back in input order: the order you passed the promises in. They don't come back in the order the promises finish. So a slow promise placed first still gets the first place.
Promise.all still lists the values in the order you passed them in: A, B, C.The replay below uses three cooking tasks. Each task is a promise with a timer:
- Task A takes 30 milliseconds.
- Task B takes 10 milliseconds.
- Task C takes 20 milliseconds.
Choose which task fails, then step through it. When one task fails, Promise.all rejects right away. This is called fail-fast. The other tasks are not stopped. Their timers still run to the end.
Choose which task fails. Predict whether Promise.all prints all three values or one failure.
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 also takes an iterable. It waits until every promise has settled, even the ones that fail. A failed input doesn't make it reject. It gives back one report object for each input:
- For a promise that fulfilled:
{ status: "fulfilled", value }. - For a promise that rejected:
{ status: "rejected", reason }.
Use it when you want to show every result, failures included. Upload reports and test results are good examples.
Promise.allSettled([ Promise.resolve("saved"), Promise.reject("offline"),]).then((results) => { console.log(results.map((item) => item.status).join(" / "));});Line 2 is a promise that fulfills with "saved". Line 3 is a promise that rejects with "offline". results holds two report objects, in input order. Line 5 reads the status of each report and prints fulfilled / rejected. The failed promise is still in the report.
Promise.all stops at the first failure. Promise.allSettled waits for both and gives you one report for each.A teacher doesn't stop collecting exams because one student failed. The class report is only useful once every paper is in. allSettled works the same way: it doesn't stop early, and no row is missing.
- In real life: Every exam paper is handed in
- In JavaScript: Every input settles before the group promise fulfills
- In real life: Some students pass, some fail
- In JavaScript: Each report has the status fulfilled or rejected
- In real life: The teacher reads out the full class report
- In JavaScript: You get an array of report objects
In the kitchen race track below, four tasks run on real timers. Pick a method to run them with and watch the timeline. You'll meet race and any in the next section.
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 to finish winsPromise.any(tasks); // first success winsPress a Run button to start the tasks.
- Nothing has finished yet.
Promise.all waits for every task to succeed. It rejects as soon as one task fails. Watch the other timers: they still finish.
Promise.race & Promise.any
SORT ITPromise.race settles as soon as the first input settles. It copies that first result:
- If the first promise to finish fulfills, the race fulfills with its value.
- If the first promise to finish rejects, the race rejects with its reason.
With an empty array, Promise.race([]) stays pending forever. There is no promise that can finish first.
Promise.any waits for the first promise that fulfills. It skips failures, as long as another promise could still succeed. If every input rejects, Promise.any rejects with an AggregateError. An AggregateError is one error that holds a list of errors. Its .errors array lists the reasons in input order. Promise.any([]) rejects right away with an AggregateError, because no promise can ever succeed.
const offline = Promise.reject("offline");const tea = Promise.resolve("tea"); Promise.race([offline, tea]).catch((reason) => console.log("race: " + reason));Promise.any([offline, tea]).then((value) => console.log("any: " + value));Line 1 is a promise that has already rejected with "offline". Line 2 is a promise that has already fulfilled with "tea". Both lines 4 and 5 get the same array:
- Line 4:
racetakes the first result, which is the failure. It printsrace: offline. - Line 5:
anyskips the failure and takes the success. It printsany: tea.
race takes the first one to finish, even a failure. any skips the failure and takes the first success.In a horse race, the first thing that happens makes the news. A horse wins, or the favorite falls. When you apply for jobs, one yes is enough. You give up only when every company says no.
- In real life: The first horse crosses the line, or falls
- In JavaScript:
Promise.raceuses the first result, success or failure - 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 with anAggregateError
- Load the profile, permissions, and settings before you show a dashboard
- Upload 8 photos and show which ones worked and which failed
- Race a timer so you stop waiting after 3 seconds
- Ask three copies of a server and use the first good answer
- Collect every exam result before you read out the class report
- Show whichever connection try finishes first, success or failure
- Wait for soup, salad, and dessert before you serve a table
- Send job applications and celebrate the first yes
Put each real task under the method that fits it best.
Here are the small cases that are easy to forget. The last line rejects on purpose.
await Promise.all(["plain", Promise.resolve("promise")]);await Promise.all([]);Promise.race([]); // stays pending foreverawait Promise.any([]); // rejects with AggregateError- Line 1: a plain value and a promise in one array. You get
["plain", "promise"]. - Line 2: an empty array. You get an empty array.
- Line 3: an empty race never settles, so the code doesn't wait for it.
- Line 4: an empty
anyrejects with anAggregateError.
Promise.all([]).then((values) => console.log("all " + values.length));Promise.allSettled([]).then((values) => console.log("settled " + values.length));Promise.any([]).catch((error) => console.log(error.name + " " + error.errors.length)); let raceSettled = false;Promise.race([]).then(() => { raceSettled = true; });setTimeout(() => console.log("race pending " + !raceSettled), 10);Lines 1 to 3 give each method an empty array. Line 5 starts an empty race, and line 6 checks it 10 milliseconds later. The output is:
all 0:Promise.allfulfilled with an empty array.settled 0:Promise.allSettleddid the same.AggregateError 0:Promise.anyrejected with anAggregateErrorthat holds no errors.race pending true: the race is still pending.
So if your array might be empty, don't pass it to Promise.race alone. Add a timer or another way to stop waiting.
Promise.withResolvers
NEWER APIUsually you make a promise with new Promise((resolve, reject) => ...). The two functions resolve and reject settle the promise. But they only exist inside those brackets.
Sometimes the code that makes the promise isn't the code that knows when to settle it. A click handler is one example. Promise.withResolvers() helps here. It gives back an object with three things:
promise: the new promise, which other code can wait on.resolve: a function that fulfills the promise.reject: a function that rejects the promise.
It works in current browsers and in Node 22. If your code must also run on older versions, check that it exists first. This check is called feature detection.
At a coffee shop you get an order number. The person at the counter has two buttons: one marks your order ready, and one marks it sold out. You wait; they press a button when they know.
- In real life: You hold an order number
- In JavaScript: The
promisethat someone can wait on - In real life: The counter marks your order ready
- In JavaScript: The
resolvefunction - In real life: The counter marks your order sold out
- In JavaScript: The
rejectfunction
const { promise, resolve } = Promise.withResolvers();resolve("order ready"); promise.then((value) => console.log(value));Line 1 makes the promise and takes out promise and resolve. Line 2 calls resolve with the value "order ready". No new Promise brackets are needed. Line 4 adds a handler. The handler runs a moment later and prints order ready.
resolve function that settles it. Line 2 calls resolve, so line 4 prints order ready.function waitForClick(button) { const { promise, resolve, reject } = Promise.withResolvers(); button.addEventListener("click", () => { resolve("button clicked"); }, { once: true }); return { promise, cancel: () => reject("cancelled") };}checking…
- Make a promise, then settle it with the buttons.
withResolvers makes the promise in one place. Other code can settle it later.
A real use is waitForClick(button). It makes a promise and fulfills it when the button is clicked. It can also hand out a cancel function that rejects the promise.
Promise.try
FEATURE DETECTA function can fail in two ways:
- It can
throwan error right away. This is a synchronous throw: it happens now, before the function returns. - It can return a promise that rejects later.
Promise.try(fn) handles both the same way. It calls fn right now and gives back a promise for its result. If fn throws right away, that promise rejects. So one .catch handles both kinds of failure.
Promise.try is new. It works in newer browsers and in Node 23, but not in Node 22. So this lesson checks whether it exists. If it doesn't, the lesson uses a small stand-in function instead. A stand-in like this is called a fallback.
A tightrope walker can fall at the first step or halfway across. Without a net, you need one plan for each kind of fall. With one net under the whole rope, both falls land in the same place.
- In real life: You trip as soon as you step onto the rope
- In JavaScript:
fnthrows right away - In real life: You slip halfway across the rope
- In JavaScript:
fnreturns a promise that rejects later - In real life: The same net catches both falls
- In JavaScript: One
.catchhandles either failure
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 what the function does, then press Run.
Promise.try calls the function right away. If the function throws right away, Promise.try turns that into a rejected promise. So one catch handles both kinds of failure. Promise.resolve().then(fn) would call fn a moment later instead.
You may also see Promise.resolve().then(fn). It also turns a throw into a rejection. But it doesn't call fn right away. It calls it a moment later, as a microtask: a small job that runs after the current code finishes. Promise.try(fn) calls fn right away.
function promiseTry(fn) { if (typeof Promise.try === "function") return Promise.try(fn); return new Promise((resolve) => resolve(fn()));} function readPrice() { console.log("read now"); return 3;} promiseTry(readPrice).then((price) => console.log("price " + price));console.log("after call");Lines 1 to 4 are the fallback helper. Line 2 uses the real Promise.try if it exists. Line 3 is the fallback: it calls fn inside new Promise, which also turns a throw into a rejection.
Line 11 calls readPrice through the helper. The output is:
read now:readPriceruns right away, from line 7.after call: line 12 runs next.price 3: the.thenhandler runs last, after the current code finishes.
Promises in an object: Promise.allKeyed
PROPOSALPromise.all gives back an array. When each place in the array means something different, it's easy to mix them up. Look at this code:
const [alerts, user] = await Promise.all([ fetchUser(), fetchAlerts(),]);// Oops: alerts contains the user, and user contains the alerts.Line 1 takes the two values out of the array by place. The first value goes into alerts, and the second goes into user. But line 2 puts fetchUser() first. So the names are swapped.
A new feature fixes this. It adds two methods, Promise.allKeyed and Promise.allSettledKeyed. You give them an object of promises, and they give back an object with the same property names. Then you read result.user, not the first place in an array.
This feature is a proposal: a planned addition that isn't part of the language yet. Firefox 155 has early, experimental support. Node 22 and most browsers don't have it.
A lunchbox with labeled parts is hard to mix up. A row of plain bowls that all look the same is easy to mix up. Keyed methods keep the names next to the results.
- In real life: Each part of the lunchbox has a label
- In JavaScript: Each property name stays with its value
- In real life: The rice and the fruit can't swap places
- In JavaScript: You read
result.user, not the first place in an array - In real life: A broken part still has its label
- In JavaScript:
allSettledKeyedgives a report under each name
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 a helper. The names stay next to their values.
allKeyed waits for an object of promises and keeps each property name next to its value. If alerts fails, allKeyed rejects, but allSettledKeyed still gives a report under each name.
You can write a small version yourself today:
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(),});- Line 1 defines the helper. It takes an object of promises.
- Line 2 saves the property names, like
["user", "stats", "alerts"]. - Line 3 waits for all the values with
Promise.all. They come back in the same order as the names. - Line 4 puts each name back next to its value and builds a new object.
The helper uses Object.keys, which lists only the text property names. The real feature also handles symbol keys. For most code, text names are all you need.
Where you'll use combinators
The method you pick tells the reader what your code needs:
Promise.allsays: "This page needs every piece."Promise.allSettledsays: "Show how each one ended."Promise.anysays: "The first success is enough."Promise.racesays: "Whatever finishes first 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 };}- Line 2 needs the user, the widgets, and the feature flags. If one fails, the dashboard can't show.
- Line 8 uploads every file and keeps a report for each one, failed or not.
- Line 9 asks several copies of the same server and takes the first one that answers well.
A common trick is to race the real work against a timer. If the timer finishes first, you stop waiting. But the slow request is not stopped; it keeps running. Real code usually adds AbortController, a tool that can stop a request. The Async patterns lesson shows the full version.
const timeout = new Promise((_, reject) => { setTimeout(() => reject(new Error("too slow")), 3000);}); const data = await Promise.race([loadData(), timeout]);Line 1 makes a promise that rejects with "too slow" after 3000 milliseconds, which is 3 seconds. Line 5 races loadData() against it. If the data comes first, data gets it. If the timer comes first, the await throws the "too slow" error.
Common misconceptions
Read each row from left to right: the method, when it fulfills, when it rejects, and what it's good for.
| Method | When does it fulfill? | When does it reject? | What is it good for? |
|---|---|---|---|
Promise.all | Every input fulfills. The values are in input order. | The first input rejects. | Data the page can't work without. |
Promise.allSettled | Every input settles. You get a report for each one. | Never because an input failed. | Reports that show every result. |
Promise.race | The first input to settle fulfills. | The first input to settle rejects. | Stopping a wait with a timer. |
Promise.any | The first input fulfills. | Every input rejects. You get an AggregateError. | Backups, where one success is enough. |
- "Promise.all stops the other promises." It doesn't. It rejects early, but the other work keeps running unless you stop it yourself.
- "The values come back in the order they finish." No.
allandallSettledkeep input order. - "allSettled gives me the values." It gives report objects. Check
statusfirst, then readvalueorreason. - "race means the first success."
racemeans the first result, success or failure. Useanyfor the first success. - "New methods work everywhere." Check that
withResolvers,Promise.try, and proposals likeallKeyedexist before you use them.
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 slow is first in the array. So the values are ["slow", "fast", "plain"], and the code prints 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 promise rejects, so any skips it. The second waits 5 milliseconds. The third has already fulfilled, so it is the first success. The code prints yes C.
What status text does it print?
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 promise fulfills and the second rejects. allSettled still fulfills, with one report for each. The code prints fulfilled/rejected.
Read the helper, then predict what it prints.
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 waits for the values with Promise.all. Then it builds a new object with the same names. So result.user is "Ada" and result.count is 3, and the code prints Ada 3.
What does the .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));The function throws inside new Promise. So the promise rejects instead of crashing, and .catch prints the message 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 method never rejects because one input failed?
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 does this lesson check that Promise.try exists before using it?
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 "every one must succeed." It keeps input order. It rejects with the reason from the first failure.Promise.allSettledis for reports. It waits for every input and gives a report object for each one.Promise.racetakes the first result, success or failure.Promise.anytakes the first success, or rejects with anAggregateError.withResolversandPromise.tryare newer. Check that they exist, and use a fallback when they don't.allKeyedis a proposal. A small helper built onObject.keysgives you the same idea today.
Final definition: A promise combinator turns many promises into one promise. You pick the one whose waiting rule matches what your code needs.
Up next: Promisification & promise-based APIs.