Skip to content

Promises

A Promise is an object representing a value that will be available in the future. It has three states:

  • Pending — the operation is still in progress.
  • Fulfilled — the operation completed successfully; the Promise holds a value.
  • Rejected — the operation failed; the Promise holds an error.

A Promise transitions from pending to either fulfilled or rejected — never back. Once settled, it stays that way.

var p = new Promise(function(resolve, reject) {
// Call resolve(value) to fulfill, or reject(error) to reject
resolve('hello');
});

.then(onFulfilled) returns a new Promise, allowing you to chain steps. .catch(onRejected) handles rejections. .finally(fn) runs regardless of outcome — useful for cleanup.

function fetchData() {
return Promise.resolve({ id: 1 });
}
function transform(data) {
return { ...data, done: true };
}
fetchData()
.then(function(data) { return transform(data); })
.then(function(result) { console.log('Done:', result); })
.catch(function(err) { console.error('Failed:', err.message); })
.finally(function() { console.log('Cleanup'); });
CombinatorBehavior
Promise.all(arr)Resolves when all resolve; rejects on the first rejection
Promise.race(arr)Settles with the first Promise to settle (fulfilled or rejected)
Promise.allSettled(arr)Resolves when all settle; gives each outcome (never rejects)
Promise.any(arr)Resolves with the first fulfilled; rejects only if all reject

The snippet creates two Promises that resolve immediately with fixed values, chains .then, uses Promise.all, and adds .finally for cleanup.

JavaScript

The output is always: Fetched: data-A, then All done: data-A and data-B, then Finally: cleanup.

You can dial that behavior with the --unhandled-rejections flag if you need to, though throw (the default) is almost always the right choice in new code:

ModeBehavior
throw (default since Node 15)Raises an uncaught exception and crashes the process
strictSame as throw, plus rejections that happen during process shutdown also crash
warnLogs a warning to stderr, does not crash — the old pre-15 default, now discouraged
noneSilences the warning entirely

When one Promise passed to Promise.all rejects, Promise.all itself settles (rejects) immediately — but it does not cancel or stop the work behind the other Promises. Their underlying operations (a timer, a fetch, a database query) keep running to completion; they just have no one left listening for the result by the time they finish:

Node.js
Output (verified on Node 24)
Promise.all rejected immediately: fast task failed
slow task finished — still ran after Promise.all had already rejected

Promise.all rejects the instant the first input rejects, but slowTask’s setTimeout was already scheduled — it fires 30 ms later regardless, and its resolve call has no effect on anything (the combined Promise already settled). If you need every result and every outcome instead of an early exit, use Promise.allSettled, which never rejects and gives you a { status, value } or { status, reason } object per input, once all of them have finished:

// @skip-verify illustrative — taskA/taskB/taskC are placeholders for your own Promise-returning functions
Promise.allSettled([taskA(), taskB(), taskC()]).then(function (results) {
results.forEach(function (r) {
if (r.status === 'fulfilled') console.log('ok:', r.value);
else console.log('failed:', r.reason.message);
});
});

race vs any, and cancelling with AbortController

Section titled “race vs any, and cancelling with AbortController”

Promise.race settles — fulfilled or rejected — with whichever input settles first. Promise.any ignores rejections and resolves with the first fulfillment, only rejecting if every input rejects:

Node.js
Output (verified on Node 24)
race rejected: fast rejection
any resolved: slow fulfillment

race settles with whatever finishes first — here, the already-rejected fast — even though a fulfillment was on the way. any skips past that rejection and waits for the first success.

None of all, race, allSettled, or any can actually cancel the losing operations on their own — a setTimeout or an in-flight fetch keeps running unless something explicitly tells it to stop. That is what AbortController is for: pass its signal into whatever supports cancellation (fetch, fs promises, or your own code via an 'abort' listener), and call controller.abort() when you no longer care about the result:

Node.js
Output (verified on Node 24)
rejected: aborted: client gave up after 20ms

The abort at 20 ms beats the 100 ms timer, clearTimeout actually stops the pending work this time (unlike the Promise.all example above), and the Promise rejects with the abort reason instead of ever resolving.

new Promise(executor) exists to wrap APIs that are not already Promise-based — a callback API, an event, a timer. If the thing inside the executor already returns a Promise, wrapping it again adds nothing but risk (a forgotten reject, a swallowed rejection):

// Needed: fs.readFile is callback-based, not Promise-based
function readFileAsPromise(path) {
return new Promise(function (resolve, reject) {
require('node:fs').readFile(path, 'utf8', function (err, data) {
if (err) reject(err); else resolve(data);
});
});
}
// Not needed: fetch already returns a Promise — this just adds risk
function getJson(url) {
return new Promise(function (resolve, reject) {
fetch(url).then(resolve, reject); // just: return fetch(url)
});
}

A Promise is a receipt for a value that is not ready yet, with exactly one of three states written on it forever once it settles. Combinators do not change how any individual Promise settles — they only describe when to stop waiting and look at the receipts: all stops at the first rejection, allSettled waits for every receipt regardless of outcome, race stops at the first settlement of any kind, any stops at the first success. None of them reach back and cancel the work a Promise represents; only AbortController (or a cancellation mechanism the underlying API itself supports) can do that.

Promise.all is the right default when you need every result and a failure anywhere should stop the whole operation — but it throws away partial success information (which ones actually worked) and does not free any resources the losers were using. allSettled keeps that information at the cost of always waiting for the slowest input, even a doomed one. AbortController adds real cancellation, but only for code written to respect it — sprinkling a signal through fetch calls is easy; threading it through your own long-running functions takes discipline everywhere you write an await or a timer.

  • “Promise.all rejecting means the other tasks stopped.” They did not stop — they simply lost their audience. Their side effects (a write, a charge, a log line) still happen.
  • “Promise.race picks the fastest successful result.” It picks whichever settles first, success or failure — a fast rejection beats a slow success every time.
  • “Wrapping a value in new Promise(...) is always the way to make something async-friendly.” Only when the thing you are wrapping is not already a Promise. Wrapping a Promise in another Promise is redundant and a common place to accidentally drop error handling.

AI-generated code frequently fires off a Promise.all (or a bare Promise-returning call) without a .catch() anywhere in the chain and without the caller awaiting it in a try/catch either — which is exactly the shape that triggers the crash described in the Callout above.

🐛 Spot the bug

function loadAllUsers(ids) {
return Promise.all(ids.map(function (id) { return fetchUser(id); }));
// no .catch() here...
}
loadAllUsers([1, 2, 3]);
// ...and the caller doesn't await it or attach .catch() either
Show the answer

If any fetchUser(id) rejects, Promise.all rejects too — and nothing in this file ever attaches a rejection handler to it. On Node 15+, that is an unhandled rejection, which crashes the process by default (--unhandled-rejections=throw), not just a silently ignored error. The fix is to attach .catch() where the Promise is created, or have the caller await it inside a try/catch — never leave a Promise chain with no rejection handler anywhere along it.

Which Promise state means the operation completed successfully?
What does Promise.all do when one of the input Promises rejects?
Which combinator resolves when ALL input Promises settle, and never rejects?
What does this print?
var fast = Promise.reject(new Error('fast rejection'));
var slow = new Promise(function (resolve) {
  setTimeout(function () { resolve('slow fulfillment'); }, 20);
});
Promise.any([fast, slow]).then(function (v) { console.log(v); });
A function does return Promise.all(ids.map(fetchUser)); with no .catch(), and the caller never awaits the result either. What happens if one fetchUser call rejects?
Why does wrapping fetch(url) in new Promise((resolve, reject) => fetch(url).then(resolve, reject)) count as an anti-pattern?