Key Takeaways
- JavaScript promise chaining passes results from one operation to the next.
then()returns a new promise. Return dependent promises so the chain waits for them.- A rejection or thrown error skips fulfillment handlers until a rejection handler handles it.
catch()can recover or propagate a failure. Understanding these rules remains useful withasync/await.
JavaScript promise chaining connects operations with then(), so one step’s result becomes the next step’s input. Returning a promise from a handler makes the next link wait for its outcome; a rejection travels forward until it reaches a rejection handler.
ES6 means ECMAScript 2015, the edition that introduced Promise. The title is historical, but the chaining rules still underpin modern asynchronous JavaScript. If promises are new to you, start with our JavaScript promises introduction.
What Is a Promise?
A promise represents one eventual outcome: a fulfillment value or a rejection reason. It can be pending, fulfilled, or rejected. Fulfilled and rejected promises are settled, and their state cannot change again.
This small factory demonstrates both outcomes:
function myPromise(x) {
return new Promise((resolve, reject) => {
if (x < 1) {
reject(new Error("x must be at least 1"));
} else {
resolve(x);
}
});
}
myPromise(1)
.then((data) => console.log(data))
.catch((error) => console.error(error));
myPromise(0)
.then((data) => console.log(data))
.catch((error) => console.error(error));For these numeric inputs, myPromise(1) logs 1, while myPromise(0) logs an Error with the message “x must be at least 1”. Reject with an Error, rather than a bare string.
Using then(): New Promises and Return Values
A handler is a callback you register to process an outcome. then(onFulfilled, onRejected) accepts fulfillment and optional rejection handlers and immediately returns a new promise.
What the handler does determines that new promise’s outcome:
- Return an ordinary value, such as a number: the new promise fulfills with that value.
- Return a promise: the new promise adopts its eventual fulfillment value or rejection.
- Return nothing: the new promise fulfills with
undefined. - Throw: the new promise rejects with the thrown value.
Promise.resolve(2)
.then((value) => value + 1)
.then((value) => {
console.log(value); // 3
})
.catch((error) => console.error(error));Using catch(): Handling Earlier Failures
catch(onRejected) is shorthand for then(undefined, onRejected) and returns a new promise. A final catch() can handle rejected promises and exceptions thrown in earlier handlers, unless an intervening rejection handler has already recovered.
For a linear flow, prefer .then(onFulfilled).catch(onRejected). Unlike the second argument to that same then(), the following catch() also sees exceptions thrown by onFulfilled.
How JavaScript Promise Chaining Works
Keep the original two-step idea: firstPromise checks an input, and secondPromise increments its result. The important line is the explicit return secondPromise(data).
function firstPromise(x) {
return new Promise((resolve, reject) => {
if (x < 1) {
reject(new Error("firstPromise requires x to be at least 1"));
} else {
resolve(x);
}
});
}
function secondPromise(x) {
return new Promise((resolve, reject) => {
const result = x + 1;
if (result === 2) {
reject(new Error("secondPromise rejected result 2"));
} else {
resolve(result);
}
});
}
function runChain(x) {
return firstPromise(x)
.then((data) => {
return secondPromise(data);
})
.then((data) => {
console.log(`Input ${x}: ${data}`);
})
.catch((error) => {
console.error(`Input ${x} failed:`, error);
});
}
runChain(2);
runChain(1);
runChain(0);- Input
2passes both checks and logsInput 2: 3. - Input
1reachessecondPromise, whose result is2, so the chain logs that function’s error. - Input
0rejects infirstPromise;secondPromisenever runs for that invocation.
These factories settle immediately to isolate chaining. Returning the second promise also works when that operation takes time: the next fulfillment handler receives its eventual value, not the promise object.
firstPromise(x)
|
| fulfillment value
v
then(data => secondPromise(data))
|
| fulfillment value
v
then(log result)
rejection or thrown error
|
+--> skip fulfillment handlers --> catch(handle error)Why You Must Return a Promise From then()
An expression-bodied arrow function returns its expression implicitly. Add braces, and you need an explicit return:
// Handler fragments using secondPromise() from the example above.
(data) => secondPromise(data); // Implicit return.
(data) => { return secondPromise(data); }; // Explicit return.
(data) => { secondPromise(data); }; // Wrong: returns undefined.The last handler disconnects the second promise from the chain. If that operation is asynchronous, later links can run before it finishes, and its rejection won’t reach the outer catch(). This is a floating promise: work the chain no longer tracks.
JavaScript Promise Error Handling and Recovery
A rejection does not necessarily end a chain. A rejection handler can supply a replacement value and let fulfillment handlers continue:
Promise.resolve(1)
.then(() => {
throw new Error("Could not load the value");
})
.then(() => {
console.log("This handler is skipped");
})
.catch((error) => {
console.error(error);
return "fallback";
})
.then((value) => {
console.log(value); // fallback
})
.catch((error) => {
console.error("Recovery failed:", error);
});The throw rejects the next link, skips the following fulfillment handler, and reaches catch(). Returning "fallback" fulfills the promise returned by catch(), so the next then() runs.
To preserve failure, use throw error or return Promise.reject(error) instead. That rejection still needs a later handler or a caller that handles it.
A Realistic Dependent-Request Chain
Suppose your browser application exposes /api/users/42, returning JSON with a numeric id, and /api/users/42/posts, returning that user’s posts. The second request depends on the first response.
// Application fragment: requires the endpoints described above.
function getJSON(url) {
return fetch(url).then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status} while loading ${url}`);
}
return response.json();
});
}
function loadUserPosts(userId) {
return getJSON(`/api/users/${userId}`).then((user) => {
if (!user || typeof user.id !== "number") {
throw new Error("User response is missing a numeric id");
}
return getJSON(`/api/users/${user.id}/posts`);
});
}
loadUserPosts(42)
.then((posts) => {
console.log(posts);
})
.catch((error) => {
console.error("Could not load posts:", error);
});fetch() returns a promise for a Response, but HTTP errors such as 404 do not automatically reject it. response.ok checks for a status from 200 through 299; throwing turns a non-success response into a rejection. response.json() also returns a promise, so return it before consuming parsed data.
loadUserPosts() returns the entire chain. Its caller handles network failures, HTTP checks, JSON parsing failures, and the explicit missing-id error in one place.
Sequential Chaining vs Promise.all()
Chain operations when one needs another’s result. For independent work, you can start both operations first and then coordinate their outcomes. That is concurrency: the operations can be in progress at the same time.
// Application fragment: reuses getJSON() above.
// Also assumes /api/site-settings returns JSON.
Promise.all([
getJSON("/api/users/42"),
getJSON("/api/site-settings"),
])
.then(([user, settings]) => {
console.log({ user, settings });
})
.catch((error) => {
console.error("Could not load page data:", error);
});The getJSON() calls start the requests; Promise.all() coordinates their promises. On success, its result array follows input order, not completion order.
| Method | Outcome | Use it when |
|---|---|---|
Promise.all() | Fulfills with all values in input order; rejects on the first rejection. | Every independent result is required. |
Promise.allSettled() | Fulfills with each input’s outcome after every input settles. | You need to inspect successes and failures. |
Promise.race() | Adopts the first settlement, whether fulfillment or rejection. | The first outcome determines your next step. |
Promise.any() | Fulfills with the first fulfillment; rejects with an AggregateError if every input rejects. | Any one successful result is enough. |
These coordinating methods do not cancel outstanding operations when their result settles. Cancel the underlying operation separately when needed; fetch() supports cancellation through AbortController.
Promise Chaining vs async/await
async/await is syntax for working with promises, not a competing error model. An async function always returns a promise. Awaiting a rejected promise throws its rejection reason at that point inside the function.
// Reuses firstPromise() and secondPromise() above.
async function runSequentially(x) {
const first = await firstPromise(x);
const second = await secondPromise(first);
return second;
}
runSequentially(2)
.then((result) => console.log(result)) // 3
.catch((error) => console.error(error));Use whichever style makes dependencies clearer. You can also await Promise.all(...) for independent work. See our JavaScript async/await example for a fuller walkthrough.
Promises, Callbacks, and Ongoing Events
Promises standardize composition and rejection propagation compared with nested callbacks; they do not guarantee that errors are handled. They do not eliminate callbacks either: your then() handlers are callbacks.
Check an API’s interface before chaining. Many APIs return promises, while others use callbacks, events, or streams. You can adapt a one-shot callback to a promise, but a single promise settles only once; it does not represent an ongoing sequence of values. For that distinction, see when to use a Promise rather than an Observable.
Common Mistakes
- Calling a handler instead of passing it. Use
.then(secondPromise), not.then(secondPromise()), when you want the previous value supplied later. - Wrapping an existing promise in another constructor. If an API already returns a promise, you usually do not need
new Promise(). Return and chain its existing promise instead. - Catching too early and pretending success. Recover only when your fallback is meaningful; otherwise rethrow.
- Assuming catch() covers unrelated work. Floating promises and unrelated event callbacks are outside the chain unless you explicitly connect their outcomes to it.
Conclusion
When you read a chain, ask three questions: what does each handler return, which operation depends on that result, and where does rejection get handled? Those questions help you reason about both direct promise chains and async/await.
FAQ
What does then() return in JavaScript?
then() returns a new promise. A handler’s returned value fulfills that promise, returning another promise makes it adopt that promise’s outcome, and throwing rejects it.
How do errors propagate through a promise chain?
A rejection or an exception thrown in a handler rejects the next promise in the chain. Fulfillment handlers are skipped until a rejection handler, often catch(), handles the failure.
Can catch() recover a rejected promise?
Yes. Returning a replacement value from catch() fulfills the promise it returns, so later fulfillment handlers run. Throwing or returning a rejected promise keeps the chain rejected.
When should I use Promise.all() instead of chaining?
Use Promise.all() when operations are independent and you need all their results. Start the operations and pass their promises to Promise.all(); chain operations when a later step depends on an earlier result.
Is async/await different from promises?
async/await is syntax for consuming promises, not an alternative to them. Async functions return promises, and await turns a rejection into a thrown value that you can handle with try/catch.
Further Reading
- MDN: Using promises — chaining, floating promises, and error propagation.
- MDN: Promise.prototype.then() — handler arguments and return-value rules.
- MDN: Promise.prototype.catch() — rejection handling and recovery.
- MDN: Promise constructor — executor behavior and callback adapters.
- MDN: Promise reference — states and coordination methods.
- MDN: Using the Fetch API — response checks, body parsing, and cancellation.
- MDN: async function — promise return values and await behavior.
- ECMAScript 2015 specification — the original standardized Promise API.
