Dr Rex speaking, from the control room above the main paddock. Every promise we have written so far has been a lone animal: one request, one
.then(), one .catch(), one result. That is fine while you are checking a single enclosure. It falls apart on the morning you have to confirm that the fences, the generators, the herd health logs and the storm forecast are all in order before the first ferry of visitors docks. Chain those four checks one after another and the park opens four waiting times later. What we need instead is a way to start every check at once and then reason about the whole herd of promises as a single unit.JavaScript has not always been able to do this, and the order in which the tools arrived is worth memorising. The language shipped with callbacks - you handed a function to an asynchronous operation and hoped it called you back - and that was the only mechanism from the very first version of the language, ES1, all the way to 2015. Promises were standardised in ES6, also known as ES2015. The
async and await keywords, which we meet in the next exercise, arrived two years later in ES2017. Last came top-level await, the ability to use await outside any function in a module, in ES2022. Callbacks, then promises, then async/await, then top-level await: that is the whole timeline, and this lesson lives at step two.The tools we are about to use are static methods of
Promise. Static means they live on the Promise constructor itself rather than on an individual promise, so you call Promise.all(...) and never somePromise.all(...). There are four of them:The opening checklist has a strict rule: every system must report back healthy, otherwise the gates stay shut.
Promise.all() exists for exactly that rule. You give it an array of promises and it gives you back one new promise, which waits for all of them to be fulfilled and then resolves with an array of their values in the same order you passed them in. If even one input promise rejects, the promise from Promise.all() rejects immediately with that first rejection reason, and the other values are discarded.It is worth naming what
Promise.all() does not do, because the confusions are common. It does not return the first promise that succeeds - that is Promise.any(), which we meet at the end of this lesson. It does not chain promises one after another - chaining is what .then() does, and chaining is precisely the slow behaviour we are escaping here. And it certainly cannot cancel anything: a promise in JavaScript has no cancel button, so when Promise.all() rejects early the other operations simply keep running unwatched. It waits for all of the promises in the array. That single sentence is the whole contract.Both outcomes need somewhere to land, so let us settle the shape of the failure branch before any code appears. Error handling on a promise always reads left to right in four pieces: first the promise itself, then
.catch( which opens the call, then the parameter list of the handler, written error =>, and finally the body that consumes it, console.error(error)), whose closing bracket also closes the .catch( we opened. Nothing else fits. The handler cannot come before the promise, and the arrow parameter cannot come after the body. Keep that order in mind - promise, .catch(, error =>, body - and you can write the failure branch of every composition in this lesson without hesitating.Every check in the control room will be simulated the same way, so let us build one small factory for them.
new Promise() takes a function with two parameters, resolve and reject: calling resolve(value) fulfils the promise with that value, calling reject(error) rejects it. setTimeout stands in for the seconds a real sensor would spend answering, and a fault flag lets us decide in advance whether a given system reports a problem, so the timings in this lesson stay predictable rather than random.1// One factory for every park check.
2// delayMs = how long the sensor takes, fault = whether it reports a problem.
3function check(name, delayMs, fault) {
4 console.log("Checking: " + name);
5 return new Promise((resolve, reject) => {
6 setTimeout(() => {
7 if (fault) {
8 reject(new Error(name + " reported a fault"));
9 } else {
10 resolve({ system: name, status: "OK", tookMs: delayMs });
11 }
12 }, delayMs);
13 });
14}Notice what
check() gives back: not a report, but a promise of a report. The function itself finishes instantly - the console.log runs, the promise object is returned, and the next line of your program continues straight away. The report only comes into existence later, when the timer fires and resolve is called. Notice also what did not change: this is an ordinary promise, exactly the kind you have been building for the last few exercises. None of the four static methods needs a special promise. They compose the plain ones you already know how to write.Before we compose anything, one call on its own, so that the failure branch is concrete rather than theoretical. We ask for a check that we know will fault, keep the returned promise in a variable, and attach the handler in exactly the order described above.
1const perimeter = check("Perimeter fence", 500, true);
2
3perimeter.catch(error => console.error(error.message));
4// After 500 ms: Perimeter fence reported a faultRead that second line as four parts:
perimeter is the promise, .catch( opens the handler, error => names the rejection reason, and console.error(error.message)) consumes it and closes the call. The reason here is an Error object, which is why we reach for .message to print just the sentence. One recommendation before we go further: always reject with new Error(...) rather than with a bare string. Rejecting with a string is legal and it will still trigger your .catch, but you throw away the stack trace and every handler downstream has to guess what kind of thing it just received.Now the checklist itself. We start all four checks first, because the moment
check() is called its timer is already running, and only then do we hand the four promises to Promise.all(). That ordering is the entire trick: the promises are created together, so the waiting happens side by side. Date.now() returns the current time in milliseconds, which lets us measure how long the whole batch took. And since the resolved value is an array in input order, we can pull the four reports out with array destructuring - const [a, b, c, d] = results - which gives each slot a name instead of leaving us to count square brackets.1function checkParkReadiness() {
2 const startedAt = Date.now();
3
4 // All four timers start here, side by side.
5 const security = check("Security fences", 2000, false);
6 const dinosaurs = check("Dinosaur health", 3000, false);
7 const power = check("Power grid", 1500, false);
8 const weather = check("Weather forecast", 1000, false);
9
10 return Promise.all([security, dinosaurs, power, weather])
11 .then(results => {
12 const seconds = Math.round((Date.now() - startedAt) / 1000);
13 const [securityReport, dinoReport, powerReport, weatherReport] = results;
14
15 return {
16 status: "PARK READY TO OPEN",
17 security: securityReport,
18 dinosaurs: dinoReport,
19 power: powerReport,
20 weather: weatherReport,
21 checkedInSeconds: seconds
22 };
23 });
24}The four checks take 2000, 3000, 1500 and 1000 milliseconds. Run them one after another and you wait 7500 milliseconds; run them side by side, as we do here, and you wait only for the slowest, so
checkedInSeconds comes back as 3. That is the reward for creating the promises before composing them. What did not change is the shape of the result: the weather report still lands in the fourth slot even though the weather check finished first, because the output array follows the input array and never the finishing order.A function that returns a promise is only half the story - somebody has to consume it. The consumer decides what the two outcomes mean for the park: a fulfilled readiness report opens the gates, a rejection sends a message to the control room instead. Both branches hang off the same call.
JSON.stringify(value, null, 2) turns the report object into readable text indented by two spaces, which is all a park log ever needs.1checkParkReadiness()
2 .then(report => {
3 console.log("PARK READINESS REPORT");
4 console.log(JSON.stringify(report, null, 2));
5 openTheGates();
6 })
7 .catch(error => {
8 console.error("Park is not ready: " + error.message);
9 notifyControlRoom(error.message);
10 });
11
12function openTheGates() {
13 console.log("Opening the main gates for visitors...");
14}
15
16function notifyControlRoom(message) {
17 console.log("Control room notified: " + message);
18}With every
fault flag left at false the report prints after roughly three seconds and the gates open. Set the flag on the weather check to true and the picture changes completely: after one second Promise.all() rejects, the .then() handler is skipped entirely, and the control room gets the message instead. What did not change are the other three checks. The fence, health and power timers are still counting down and will still call resolve a second or two later, into a promise nobody is listening to. A rejection stops your pipeline; it does not stop the work.The order guarantee deserves its own demonstration, because it is the part people distrust. The promise returned by
Promise.all() resolves with an array whose slots line up with the array you passed in, no matter which promise settled first. To prove it, here are three dinosaur record lookups with deliberately mismatched response times: the second one answers in 100 milliseconds while the first one takes 900. If the output followed the finishing order, the Velociraptor would come back in slot zero.1function fetchDino(id) {
2 const names = { 1: "T-Rex", 2: "Velociraptor", 3: "Triceratops" };
3 const delays = { 1: 900, 2: 100, 3: 500 };
4 return new Promise(resolve => {
5 setTimeout(() => resolve({ id: id, name: names[id] }), delays[id]);
6 });
7}
8
9Promise.all([fetchDino(1), fetchDino(2), fetchDino(3)])
10 .then(([first, second, third]) => {
11 console.log("Slot 0: " + first.name);
12 console.log("Slot 1: " + second.name);
13 console.log("Slot 2: " + third.name);
14 });
15// Slot 0: T-Rex
16// Slot 1: Velociraptor
17// Slot 2: TriceratopsThe Velociraptor record arrived first and the T-Rex record arrived last, and yet slot zero still holds the T-Rex. Position in the output array is decided by position in the input array and by nothing else. That is what makes the destructuring pattern
([first, second, third]) safe: you are naming positions, not winners. Compare it with Promise.race() in the next section, where the finishing order is the entire point, and you have the cleanest possible way to remember the difference between the two methods.One more thing before we leave
Promise.all(), because it explains a surprise that catches almost everyone. When a promise settles, its .then() handler is not called on the spot, and it is not queued alongside timers either. It goes into a separate queue called the microtask queue, and the engine drains that queue completely before it touches anything else. setTimeout() and setInterval() schedule macrotasks, the ordinary and slower queue, and requestAnimationFrame() schedules a callback for just before the browser paints the next frame. Not one of those three is a microtask. The promise callback is the microtask here. Promise.resolve(value) below creates a promise that is already fulfilled, so nothing in this snippet waits for anything.1console.log("1: control room script starts");
2
3setTimeout(() => {
4 console.log("4: setTimeout callback - a macrotask");
5}, 0);
6
7Promise.resolve("radio check").then(value => {
8 console.log("3: then callback - a microtask - " + value);
9});
10
11console.log("2: control room script ends");The output is 1, 2, 3, 4, and the
setTimeout loses even with a delay of zero. The synchronous lines run first, then the microtask queue drains, and only then does the engine pick up the next macrotask. This is why a whole chain of .then() handlers can finish before a zero-delay timer that was scheduled earlier. What did not change is the promise itself: it was already settled the instant it was created, so what you are watching is scheduling, not waiting. Keep the split straight - .then() is a microtask, timers and animation frames are not.The all-or-nothing rule that makes
Promise.all() perfect for an opening checklist makes it wrong for a status board. One failing camera should not erase the readings from the thirty-five that work, and one unreachable feeding sensor should not blank out the entire herd report. Whenever the operations are genuinely independent, whenever you want the failures listed rather than fatal, Promise.all() is the wrong instrument and Promise.allSettled() further down this page is the right one. The rule of thumb is short: reach for Promise.all() only when the next step is impossible without every single result.The paddock cameras have picked up an animal that should not be loose, and the control room needs a species name now, not the most accurate name eventually. Three identification systems can answer: a visual classifier, a DNA match and a behavioural analyser. They differ in speed and in confidence, and in an emergency we would rather act on a fast answer than stand still waiting for a perfect one.
Promise.race() takes an array of promises and returns a promise that settles the moment the first of them settles. First is meant literally: whichever promise finishes first decides the outcome, and if that first one happens to reject, the race rejects with it. It does not hold out for a success and it never looks at the losers. That makes it the right tool for two jobs, taking the first available answer and putting a deadline on an operation, and the wrong tool for everything else.Here are the three identifiers. Each resolves with the method that produced the answer, the species and a confidence between zero and one. The delays are the ones the park technicians measured last season, and they are fixed rather than random so that the winner of the race is predictable while you read.
1function recognizeVisually() {
2 console.log("Visual analysis started");
3 return new Promise(resolve => {
4 setTimeout(() => {
5 resolve({ method: "visual", species: "Velociraptor", confidence: 0.82 });
6 }, 1800);
7 });
8}
9
10function recognizeByDna() {
11 console.log("DNA analysis started");
12 return new Promise(resolve => {
13 setTimeout(() => {
14 resolve({ method: "dna", species: "Velociraptor", confidence: 0.98 });
15 }, 3500);
16 });
17}
18
19function recognizeByBehaviour() {
20 console.log("Behavioural analysis started");
21 return new Promise(resolve => {
22 setTimeout(() => {
23 resolve({ method: "behavioural", species: "Velociraptor", confidence: 0.75 });
24 }, 2200);
25 });
26}Three separate promises, three separate timers, and so far no relationship between them at all. The visual classifier is the quickest at 1800 milliseconds and the least sure of itself; the DNA match is the slowest at 3500 milliseconds and the most trustworthy; the behavioural analyser sits in the middle on both counts. That spread is deliberate, because it is exactly the bargain a race forces on you: you are buying speed with accuracy. Notice that nothing in these three functions is race-specific. Each is an ordinary promise-returning function and each would work unchanged inside
Promise.all().Now the race itself. All three calls sit inside the array literal, which means all three timers start together, and
Promise.race() simply reports whichever one finishes first. Math.round() rounds a number to the nearest integer, which we use to turn the confidence into a clean percentage instead of a trail of floating point digits.1Promise.race([
2 recognizeVisually(),
3 recognizeByDna(),
4 recognizeByBehaviour()
5])
6 .then(result => {
7 console.log("Identified as " + result.species);
8 console.log("Method: " + result.method +
9 ", confidence: " + Math.round(result.confidence * 100) + "%");
10 });
11// After 1800 ms:
12// Identified as Velociraptor
13// Method: visual, confidence: 82%The visual classifier wins at 1800 milliseconds and the control room can raise the raptor paddock alert immediately. What did not change are the other two analyses: the DNA match still completes at 3500 milliseconds and the behavioural one at 2200, because
Promise.race() has no way to stop them. It only stops listening. If the losing operations are expensive, a race saves you the wait but never the cost. And had the visual classifier rejected at 1800 milliseconds, the race would have rejected too, even though a perfectly good DNA answer was 1700 milliseconds away.The second job for
Promise.race() is the timeout, and it is the reason most codebases use the method at all. JavaScript gives you no built-in way to abandon a slow promise, but you can race it against a promise whose only purpose is to reject after a set number of milliseconds. Look at the first parameter of the executor below: it is written _, because a timeout promise never resolves and an underscore is the conventional name for a parameter you are forced to declare but will never use.1// A promise whose only job is to fail on schedule.
2function timeoutAfter(ms, label) {
3 return new Promise((_, reject) => {
4 setTimeout(() => {
5 reject(new Error(label + " timed out after " + ms + "ms"));
6 }, ms);
7 });
8}On its own this function is useless, a promise that can only ever fail. Its whole value comes from being one of the runners in a race. Put it alongside a real operation and you get a promise with two possible endings: the real value if the operation is quick enough, or a timeout
Error if it is not. Passing the label in as a parameter is deliberate rather than decorative. When three different timeouts can fire inside the same request, a message that names the operation is the difference between a two-minute diagnosis and an hour of guessing.Here is the guard in action against a sensor that has decided to take five seconds. We give it a deadline of two, and we make the failure branch genuinely useful by falling back to the last known reading instead of leaving the dashboard blank.
1function readSensor(id, responseMs) {
2 return new Promise(resolve => {
3 setTimeout(() => resolve({ sensorId: id, celsius: 31.4 }), responseMs);
4 });
5}
6
7Promise.race([
8 readSensor("TEMP-42", 5000),
9 timeoutAfter(2000, "TEMP-42 read")
10])
11 .then(data => console.log("Live reading: " + data.celsius))
12 .catch(error => {
13 console.error(error.message);
14 console.log("Falling back to the last known value: 30.9");
15 });
16// After 2000 ms:
17// TEMP-42 read timed out after 2000ms
18// Falling back to the last known value: 30.9Two seconds in, the timeout promise rejects, the race rejects with it, and the dashboard shows the cached figure instead of freezing. Three seconds after that the sensor finally answers, and its
resolve call lands on a promise nobody is watching: the timeout bounded our patience, not the sensor's work. That distinction matters much more when the losing operation is a database write than when it is a temperature reading. Put a deadline on anything that leaves your process, because a slow answer you can recover from always beats a request that never returns.The night shift needs something
Promise.all() cannot give it. Once an hour every system in the park is polled and a status board is printed, and the board is only useful if it lists them all: the healthy ones, the faulty ones, and exactly what went wrong. Under Promise.all() a single dead camera would reject the whole batch and leave the board empty at the moment it matters most.Promise.allSettled(), added to the language in ES2020, takes the same kind of array but never rejects. It waits until every promise has settled - fulfilled or rejected, it does not care which - and then resolves with an array of description objects, one per input promise, in input order. A fulfilled promise is described as { status: "fulfilled", value: ... } and a rejected one as { status: "rejected", reason: ... }. The word settled is the key to the whole method: fulfilled and rejected are the two ways a promise can settle, and this one waits for settling of either kind.Before the poll itself, the park needs a rule for turning a list of faults into a single word the night manager can act on. Not all systems are equal: the electric fence is the only thing between the visitors and the carnivores, so a fence fault is critical no matter how healthy everything else looks. Below that, the rule is a simple count. Two helpers do the work:
some() walks an array and returns true as soon as one element passes its test, and indexOf on a string returns the position of a substring or -1 when it is absent, so a result of 0 means the message begins with the text we asked about.1// Turn a fault list into one word the night manager can act on.
2function threatLevel(report) {
3 const fenceDown = report.faulty.some(message =>
4 message.indexOf("Electric fence") === 0
5 );
6
7 if (fenceDown) return "CRITICAL";
8 if (report.faulty.length > 2) return "HIGH";
9 if (report.faulty.length > 0) return "MEDIUM";
10 return "LOW";
11}The order of those four returns is the entire logic. The most severe condition is tested first and each
return ends the function immediately, so a fence fault can never be downgraded to MEDIUM by a lucky count. Notice too that this helper is completely synchronous: it takes a finished report and returns a string, with no promise anywhere in sight. That is a habit worth keeping. Composition methods belong at the edge of your program where the waiting happens, while the decisions you make about the results are ordinary code, and ordinary code is easier to read and far easier to test when it is not tangled up with asynchronous plumbing.Now the poll. We reuse the
check() factory from the opening checklist for all eight park systems, and two of them are given a fault flag so the board has something to report. The forEach loop then walks the description array and sorts each entry into one of two lists, reading result.value when the status is "fulfilled" and result.reason when it is "rejected". You have to read the right one: a rejected entry has no value and a fulfilled entry has no reason.1function monitorEverything() {
2 const monitors = [
3 check("Electric fence", 900, false),
4 check("Power grid", 700, false),
5 check("Camera network", 1200, true),
6 check("Motion sensors", 400, false),
7 check("Water system", 600, false),
8 check("Ventilation", 800, true),
9 check("Temperature control", 500, false),
10 check("Paddock gates", 1000, false)
11 ];
12
13 return Promise.allSettled(monitors).then(results => {
14 const report = { healthy: [], faulty: [] };
15
16 results.forEach(result => {
17 if (result.status === "fulfilled") {
18 report.healthy.push(result.value.system);
19 } else {
20 report.faulty.push(result.reason.message);
21 }
22 });
23
24 report.allHealthy = report.faulty.length === 0;
25 report.threatLevel = threatLevel(report);
26 return report;
27 });
28}Eight promises go in, eight description objects come out, and not one of them is lost. The two faulty systems arrive as rejected entries carrying an
Error in reason, the six healthy ones as fulfilled entries carrying our report object in value, and all eight sit in the order they were listed rather than the order they answered. What did not change is the promises themselves: check() still rejects for the camera network exactly as it did under Promise.all(). The whole difference lives in the collector, which treats a rejection as a data point instead of as a reason to stop.The status board reads the finished report and prints it. Because
monitorEverything() returns a promise that cannot reject, there is nothing here for a .catch() to do, and that is precisely the trade Promise.allSettled() asks you to accept: you never handle a failure, so you have to remember to go looking for one in the results yourself.1monitorEverything().then(report => {
2 console.log("Healthy systems: " + report.healthy.length);
3 console.log("Faulty systems: " + report.faulty.length);
4 report.faulty.forEach(message => console.log(" - " + message));
5 console.log("Threat level: " + report.threatLevel);
6});
7// Report printed after about 1200 ms:
8// Healthy systems: 6
9// Faulty systems: 2
10// - Camera network reported a fault
11// - Ventilation reported a fault
12// Threat level: MEDIUMSix healthy, two faulty, threat level MEDIUM: the fence is fine and the fault count is not above two, so the park stays open while technicians are sent to the camera network and the ventilation. Run exactly the same eight checks through
Promise.all() and you get none of this. The camera failure at 1200 milliseconds would reject the batch, the board would print one error message about one system, and the other seven results would be thrown away unread. That is the practical difference between the two methods, and it is worth seeing side by side.The clearest comparison needs no timers at all.
Promise.resolve(value) hands you a promise that is already fulfilled and Promise.reject(error) one that is already rejected, so we can build a mixed array of four known outcomes and give the very same array to both methods. Two of the four are failures, which under one method is a catastrophe and under the other is just information.1const mixed = [
2 Promise.resolve("Sector A clear"),
3 Promise.reject(new Error("Sector B fence down")),
4 Promise.resolve("Sector C clear"),
5 Promise.reject(new Error("Sector D camera dead"))
6];
7
8Promise.all(mixed)
9 .then(results => console.log("Everything clear: " + results))
10 .catch(error => console.log("Promise.all stopped at: " + error.message));
11// Logs exactly one line:
12// Promise.all stopped at: Sector B fence down
13
14Promise.allSettled(mixed).then(results => {
15 results.forEach((result, index) => {
16 if (result.status === "fulfilled") {
17 console.log("Slot " + index + ": OK - " + result.value);
18 } else {
19 console.log("Slot " + index + ": FAILED - " + result.reason.message);
20 }
21 });
22});
23// Logs all four lines:
24// Slot 0: OK - Sector A clear
25// Slot 1: FAILED - Sector B fence down
26// Slot 2: OK - Sector C clear
27// Slot 3: FAILED - Sector D camera deadPromise.all() prints one line and stops at slot one, never reaching its .then(); Promise.allSettled() prints all four and names both failures alongside both successes. Neither method treated the promises differently - the array is literally the same array - so the only variable is how much information you asked for. Which of the two groups of lines shows up first in your console is not worth worrying about, incidentally: both handlers are microtasks, they queue within a fraction of a millisecond of each other, and the exact interleaving is a scheduling detail rather than a promise about ordering. Choose Promise.all() when a failure makes the next step impossible and you want to fail fast. Choose Promise.allSettled() when failures are news rather than a dead end. And if you ever catch yourself wrapping every promise in its own .catch() just to survive Promise.all(), you wanted Promise.allSettled() all along.The storm has taken out power to half the perimeter and the visitors need to leave through an emergency gate. There are four of them. It does not matter which one opens; it matters that one opens quickly, and that a jammed gate does not stop us trying the others.
Promise.race() is exactly the wrong choice here, because the first gate to answer may well be the first gate to report itself broken, and the race would reject on that answer and leave a corridor full of people.Promise.any(), added in ES2021, is the method for this situation. It takes an array of promises and resolves with the value of the first one to be fulfilled, stepping over every rejection on the way. It rejects only when all of them have rejected, and the rejection reason is then an AggregateError, a standard error type that carries an errors property holding every individual failure in an array. One sentence separates it from its neighbour: Promise.race() waits for the first promise to settle, Promise.any() waits for the first promise to succeed.The gate tester follows the same pattern as
check(), but it deserves its own function because here the failures are the interesting part. Each gate gets a response time and a flag saying whether its motor actually engages. In this drill only gate three is in working order, and it is also the slowest of the four to answer, which is what makes the drill worth running.1function testGate(id, delayMs, works) {
2 console.log("Testing emergency gate #" + id);
3 return new Promise((resolve, reject) => {
4 setTimeout(() => {
5 if (works) {
6 resolve({ id: id, status: "operational", openingTimeS: 1.4 });
7 } else {
8 reject(new Error("Gate #" + id + " does not respond to the open signal"));
9 }
10 }, delayMs);
11 });
12}Four calls to this function will produce three rejections and one fulfilment, and crucially the rejections arrive first: gate one fails at 400 milliseconds, gate two at 700, gate four at 900, and only at 1100 milliseconds does gate three report itself operational. Under
Promise.race() the drill would end at 400 milliseconds with a rejection and nobody evacuated. Promise.any() will step over all three failures and hand us the single gate that works, which is the behaviour an evacuation actually needs from its code.Here is the drill. Both branches are worth writing even though only one of them can run today: the fulfilment branch starts the evacuation, and the rejection branch is the moment nobody wants, when every gate has failed and an
AggregateError arrives carrying the full list of reasons. map turns each error into its message and join glues the messages into one line.1Promise.any([
2 testGate(1, 400, false),
3 testGate(2, 700, false),
4 testGate(3, 1100, true),
5 testGate(4, 900, false)
6])
7 .then(gate => {
8 console.log("Evacuating through gate #" + gate.id);
9 console.log("Opening time: " + gate.openingTimeS + "s");
10 })
11 .catch(error => {
12 console.error("CRITICAL: every emergency gate failed");
13 console.error(error.errors.map(e => e.message).join(" | "));
14 });
15// After 1100 ms:
16// Evacuating through gate #3
17// Opening time: 1.4sGate three wins at 1100 milliseconds and the evacuation begins, with the three earlier failures quietly discarded. What did not change is those failures: they still happened, and if you need them for a maintenance log then
Promise.any() is not where you will find them, because a successful run throws its rejection reasons away. That is the honest cost of this method - it is optimistic by design. When the failures matter as much as the success does, reach for Promise.allSettled() instead and pick the winner out of the results yourself.The unhappy ending deserves its own look, because
AggregateError behaves differently from every error we have handled so far. Instead of one message describing one failure, it carries an errors array holding every rejection reason in input order, and name tells you which kind of error you are holding. Both gates below are jammed, so there is no fulfilment to report at all.1Promise.any([
2 testGate(5, 300, false),
3 testGate(6, 500, false)
4])
5 .catch(error => {
6 console.log(error.name);
7 console.log(error.errors.length);
8 console.log(error.errors[0].message);
9 });
10// After 500 ms:
11// AggregateError
12// 2
13// Gate #5 does not respond to the open signalTwo rejections in, two rejections out, and the
.catch() fires only once the last of them has arrived - at 500 milliseconds, not at 300. This is the one case where Promise.any() is slower than Promise.race(), and it makes sense, because to be certain that nothing will succeed it has to wait for everything. Reading error.message on an AggregateError gives you a generic summary rather than a diagnosis, so when a fallback chain runs out of options, always log error.errors.Four methods, four different questions about the same array of promises:
Promise.all()
Promise.race()
Promise.allSettled()
Promise.any()
AggregateError with every reason, only once all have rejected| Scenario | Recommended method | |----------|--------------------| | Several tasks that must all succeed, such as an opening readiness check |
Promise.all() |
| The first available answer, no matter which source produced it | Promise.race() |
| Putting a deadline on a slow operation | Promise.race() with a timeout promise |
| A full report on every operation, including the ones that failed | Promise.allSettled() |
| The first success from several fallback sources, ignoring failures | Promise.any() |Real parks do not have four dinosaurs, they have dozens, and you rarely know the count while you are writing the code. The array you hand to a composition method is an ordinary array, so build it the ordinary way:
map walks a list of identifiers and returns a new array in which every identifier has been replaced by the promise it produced. That new array of promises is exactly what Promise.all() expects. The lookup below is deterministic, giving each record a diet based on its identifier, so the statistics that follow are the same on every run.1function fetchDinoRecord(id) {
2 const diets = ["carnivore", "herbivore", "omnivore"];
3 return new Promise(resolve => {
4 setTimeout(() => {
5 resolve({ id: id, name: "Dino-" + id, diet: diets[id % 3] });
6 }, 100);
7 });
8}
9
10function fetchAllDinosaurs(ids) {
11 console.log("Fetching records for " + ids.length + " dinosaurs...");
12 const promises = ids.map(id => fetchDinoRecord(id));
13 return Promise.all(promises);
14}The important line is the one with
map. By the time it finishes, ten lookups are already in flight, and Promise.all() merely collects them. What did not change is the guarantee we proved earlier: the resolved array still matches the order of ids, so record number seven is at index six whether it answered first or last. Be aware of the flip side, though. Ten parallel requests are fine, but the same one-line map over ten thousand identifiers will open ten thousand connections at once, and we deal with that problem at the end of this section.With the fetch in place, the herd statistics are just synchronous array work on the resolved values.
filter builds a new array containing only the elements that pass a test, so counting the carnivores is a filter followed by a .length. The .catch() at the end covers all ten lookups at once, which is one of the quiet benefits of Promise.all(): one failure handler for the whole batch instead of ten.1const herdIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
2
3fetchAllDinosaurs(herdIds)
4 .then(herd => {
5 const carnivores = herd.filter(d => d.diet === "carnivore").length;
6 const herbivores = herd.filter(d => d.diet === "herbivore").length;
7 const omnivores = herd.filter(d => d.diet === "omnivore").length;
8
9 console.log("Records fetched: " + herd.length);
10 console.log(carnivores + " carnivores, " + herbivores +
11 " herbivores, " + omnivores + " omnivores");
12 })
13 .catch(error => console.error("Record fetch failed: " + error.message));
14// Fetching records for 10 dinosaurs...
15// Records fetched: 10
16// 3 carnivores, 4 herbivores, 3 omnivoresTen records arrive after roughly 100 milliseconds rather than a full second, because the ten timers ran side by side. What did not change is the failure rule: if one single record lookup rejected, the whole
.then() would be skipped and the park would learn nothing about the other nine. For a statistics screen that is arguably too strict, which brings us to the most useful advanced move of all - combining two composition methods so that each layer of your data gets the behaviour it deserves.Nothing stops you nesting these methods, and the park monitor is the natural example. Within one sector a dinosaur whose tag has gone quiet must not hide the others, so
Promise.allSettled() is right at that level. Across sectors, however, a partial map of the park is worse than useless, so Promise.all() is right at the outer level. One further piece of syntax appears below: an arrow function that returns an object literal has to wrap it in brackets, written results => ({ ... }), otherwise JavaScript reads the opening brace as the start of a function body.1function fetchSectors() {
2 return Promise.resolve([
3 { id: "A", name: "Raptor Ridge", dinosaurs: [{ id: 1 }, { id: 2 }] },
4 { id: "B", name: "Herbivore Valley", dinosaurs: [{ id: 3 }, { id: 4 }] }
5 ]);
6}
7
8function checkDinosaurStatus(id) {
9 if (id === 3) {
10 return Promise.reject(new Error("Dino-3 tag signal lost"));
11 }
12 return Promise.resolve({ id: id, status: "calm" });
13}
14
15function monitorSectors() {
16 return fetchSectors().then(sectors => {
17 const sectorPromises = sectors.map(sector => {
18 const dinoPromises = sector.dinosaurs.map(dino =>
19 checkDinosaurStatus(dino.id)
20 );
21
22 // Inside a sector: one missing tag must not hide the rest.
23 return Promise.allSettled(dinoPromises).then(results => ({
24 sector: sector.id,
25 name: sector.name,
26 results: results
27 }));
28 });
29
30 // Across sectors: we need them all, so all-or-nothing is correct.
31 return Promise.all(sectorPromises);
32 });
33}Read the nesting from the inside out. Each sector produces a promise of a small settled report, those sector promises go into an array, and
Promise.all() waits for the lot. Because Promise.allSettled() never rejects, the lost tag in Herbivore Valley can never reach the outer Promise.all() and can never spoil the map - the failure is contained at exactly the level where it belongs. What did not change is checkDinosaurStatus, which still rejects for Dino-3 as it always did. Choosing the collector, not rewriting the operation, is how you tune failure behaviour in asynchronous JavaScript.Consuming the nested result is straightforward once you remember that the inner arrays hold status objects rather than plain values. Counting the rejected entries per sector turns the structure into the one line a night manager actually wants to read.
1monitorSectors().then(sectors => {
2 sectors.forEach(sector => {
3 const lost = sector.results.filter(r => r.status === "rejected").length;
4 const tracked = sector.results.length - lost;
5 console.log(sector.name + ": " + tracked + " tracked, " + lost + " lost");
6 });
7});
8// Raptor Ridge: 2 tracked, 0 lost
9// Herbivore Valley: 1 tracked, 1 lostBoth sectors report, and the sector with a problem reports the problem instead of erasing itself. This shape scales: swap the outer
Promise.all() for Promise.allSettled() and even a completely unreachable sector becomes a line on the board rather than a crash. The general lesson is that composition methods are not alternatives you pick once for a whole program. They are decisions you make separately at every level of your data, and the right answer at one level is very often the wrong answer one level up.Starting everything at once stops being clever somewhere between ten operations and ten thousand. Fifty simultaneous requests will exhaust a connection pool, trip a rate limit or simply time out together, and
Promise.all() has no throttle. The fix is to slice the work into batches and run one batch at a time: parallel inside a batch, sequential between batches. The function below previews async and await from the next exercise - for now just read await as "pause here until this promise settles, then continue with its value". slice(start, end) copies a section of an array without touching the original.1async function runInBatches(tasks, maxParallel) {
2 const results = [];
3
4 for (let i = 0; i < tasks.length; i += maxParallel) {
5 const batch = tasks.slice(i, i + maxParallel);
6
7 // Parallel inside the batch, sequential between batches.
8 const batchResults = await Promise.allSettled(batch.map(task => task()));
9 results.push(...batchResults);
10
11 const done = Math.floor(i / maxParallel) + 1;
12 const total = Math.ceil(tasks.length / maxParallel);
13 console.log("Batch " + done + " of " + total + " finished");
14 }
15
16 return results;
17}The detail that makes this work is easy to miss:
tasks is an array of functions, not an array of promises. A promise starts the moment it is created, so an array of promises would already be running and there would be nothing left to throttle. By storing functions and calling them with task() only when their batch comes up, we control exactly when each operation starts. Promise.allSettled() inside the loop is deliberate too, since one failing task should not abort the remaining batches. What did not change is the result format: you still get one status object per task, in the original order.The call site now needs a list of functions rather than a list of promises.
Array.from({ length: 50 }, callback) builds an array of fifty elements, handing the callback each index, and our callback returns a function that has not run yet. The reused checkDinosaurStatus from the previous section still rejects for Dino-3, so exactly one signal is lost.1const herdTasks = Array.from({ length: 50 }, (_, index) =>
2 () => checkDinosaurStatus(index + 1)
3);
4
5// At most 10 checks in flight at any moment.
6runInBatches(herdTasks, 10).then(results => {
7 const lost = results.filter(r => r.status === "rejected").length;
8 console.log("Checked " + results.length + " dinosaurs, lost signals: " + lost);
9});
10// Batch 1 of 5 finished
11// Batch 2 of 5 finished
12// Batch 3 of 5 finished
13// Batch 4 of 5 finished
14// Batch 5 of 5 finished
15// Checked 50 dinosaurs, lost signals: 1Fifty checks complete with never more than ten in flight, and the one lost tag is reported instead of aborting the sweep. Ten is not a magic number: pick it from the limit you are actually protecting, whether that is a database connection pool, an API quota or the patience of a park technician. What did not change is the outcome - all fifty tasks ran and all fifty results came back. The only thing batching changed is the pressure you put on the system while getting them, and that is usually the difference between a monitor that works at park scale and one that only works in a demo.
The four static methods of
Promise are four answers to four different questions about a group of asynchronous operations. Promise.all() asks whether everything succeeded and gives up at the first failure. Promise.race() asks what finished first and accepts a failure as an answer. Promise.allSettled() asks what happened to each one and refuses to fail at all. Promise.any() asks whether anything succeeded and only gives up when nothing did. Underneath, they all take the same plain promises and they all leave the operations themselves untouched: none of them can cancel work already in flight, and none of them changes when a callback runs, since a promise callback is always a microtask that jumps ahead of any timer.Choosing between them is a design decision, not a style preference. Ask what the next step needs. If it is impossible without every result, use
Promise.all(). If it needs a deadline, race the operation against a timeout. If it is a report, settle everything. If it just needs one working answer out of several sources, take any of them. Then remember that the choice repeats at every level of nesting, and that a batching loop is what keeps the whole thing from stampeding.In the next exercise we meet the
async and await keywords, which let you write all of this in a shape that reads like ordinary sequential code while behaving exactly as it does here.A good park keeper never watches one dinosaur at a time - and now neither does your code.