Promise chaining
Learn how JavaScript promise chains run async steps in order by returning values, promises, and thenables from then handlers, plus the mistakes that break a chain.
- 01Return the next batonExplain what the next then receives when a handler returns a value, a promise, a thenable, or nothing.
- 02Read flattening correctlyKnow why returned promises and thenables are adopted instead of being nested promises.
- 03Avoid common chain breaksSpot missing returns, accidental immediate calls, pyramid nesting, and branching that is not sequencing.
Chains pass the baton
A promise chain is a row of .then calls where each step receives the value from the previous step and decides what to hand on. The key word is return. The value a handler returns becomes the value the next handler receives. If it returns a promise, the chain waits for that promise first.
A promise chain runs async steps in order because every then returns a new promise that adopts whatever its handler returns.
Picture three runners. Runner 1 gets "start", adds a label, and hands it to runner 2. Runner 2 can hand on a plain baton, wait before handing one on, or forget to hand anything over. Runner 3 simply receives whatever made it through.
- In real life: The first runner starts with the baton
- In JavaScript: The first promise fulfills with a value
- In real life: Each runner receives the baton
- In JavaScript: Each
thenhandler receives a value - In real life: Each runner hands on a baton
- In JavaScript: Each handler’s
returnbecomes the next value - In real life: Dropping the baton confuses the next runner
- In JavaScript: Forgetting
returnpassesundefined
Where the analogy stops: Real relay handoffs can be instant and physical. Promise handoffs never run inline with the current script; each handler runs later as promise microtask work.
The Promises lesson introduced then, catch, and finally on a single promise. This lesson zooms in on what happens when those handlers return values and you connect several steps. Error travel gets only a quick preview here; the next lesson, Error handling with promises, studies rejections in detail.
Returning values & promises
STEP THROUGHthen always returns a new promise. That new promise is fulfilled or rejected based on what your handler does. The everyday cases are straightforward:
| Handler behavior | New promise becomes | Next then receives |
|---|---|---|
| Returns a plain value | Fulfilled with that value | That value |
| Returns a promise | Adopts that promise’s state | The fulfilled value, or skips to catch if rejected |
| Returns a thenable | Adopts the thenable’s result | Whatever the thenable resolves with |
| Returns nothing | Fulfilled with undefined | undefined |
| Throws | Rejected with the thrown error | The next catch, not the next success handler |
Step through the race. Change step 2 before pressing Replay. Your job is to predict line 15: what does step 3 receive?
Follow the baton through three promise handlers. Change step 2 and predict what step 3 receives.
script
function delay(value) { return Promise.resolve(value);} .then((value) => { console.log("step 1 received: " + value); return value + " → step 1"; }) .then((value) => { console.log("step 2 received: " + value); return delay(value + " → step 2"); }) .then((value) => { console.log("step 3 received: " + value); return value + " → step 3"; }) .then((value) => console.log("done: " + value));Notice two important details. First, the second then does not change the original promise; it creates a new promise that follows step 2’s return. Second, returning a promise does not give the next handler a promise-in-a-box. The chain waits and hands on the fulfilled value.
Thenables: promise-shaped values
INTERACTIVEJavaScript also recognizes thenables: objects or functions with a callable then method. They are not native promises, but promise resolution treats them as promise-shaped values. That lets older libraries or custom objects plug into promise chains.
A thenable is like a printed receipt with an order number. The counter accepts it because it has a then method that can say ready or sold out.
- In real life: The counter accepts its own order token
- In JavaScript: A chain accepts a native Promise
- In real life: It also accepts a printed receipt with a number
- In JavaScript: A chain accepts an object with a
thenmethod - In real life: The counter checks whether the order is ready
- In JavaScript: Promise resolution calls the thenable’s
then
Where the analogy stops: A counter can check a receipt right away. Thenable adoption happens through promise jobs, so do not depend on exact microtask tick counts.
const thenable = {
then(resolve) {
resolve(5);
resolve(99);
},
};
Promise.resolve("ticket")
.then(() => Promise.resolve(5))
.then((value) => console.log("promise: " + value));
Promise.resolve("ticket")
.then(() => thenable)
.then((value) => console.log("thenable: " + value));The thenable above calls resolve(5) and then tries resolve(99). Only the first resolve counts. If a thenable throws before resolving, the adopting promise rejects:
const thenable = {
then(resolve) {
throw new Error("broken ticket");
resolve(5);
},
};
Promise.resolve()
.then(() => thenable)
.catch((error) => console.log(error.message));Professional note: Promise.resolve(p) returns p itself when p is a native promise. By contrast, new Promise((resolve) => resolve(p)) creates a different promise that adopts p. Both eventually settle the same way, but they are not the same object.
Flattening keeps chains flat
SORT ITWhen you get an order token, you do not collect another token when it is ready. You collect the food. Promise chains use the same flattening idea.
- In real life: The counter gives you food
- In JavaScript: A handler returns a plain value
- In real life: The counter gives you an order token
- In JavaScript: A handler returns a promise
- In real life: You wait until that token is ready
- In JavaScript: The chain adopts the returned promise
- In real life: You collect food, not another token
- In JavaScript: The next then receives the fulfilled value
Where the analogy stops: Food collection is physical. Promise adoption still schedules work, so avoid saying exactly how many microtasks a thenable takes.
Flattening applies to one returned promise or thenable. It does not recursively wait for every promise found inside every JavaScript value. An array of promises is just an array; waiting for many promises is the job of the Promise combinators lesson.
return 5return Promise.resolve(5)return { then(resolve) { resolve(5); } }no returnreturn [p1, p2].then(step())
Sort each handler result by the value received by the next fulfillment handler.
Nested, flat, or branched?
COMPAREA returned promise lets you write async steps top to bottom instead of building a sideways pyramid. The nested version below works, but every new async step adds another level of indentation. The flat version returns the inner promise so the outside chain can wait for it.
function getUser() {
return Promise.resolve({ id: 7, name: "Ada" });
}
function getOrders(userId) {
return Promise.resolve(["tea", "cake"]);
}
// Nested pyramid: works, but the shape grows sideways.
getUser().then((user) => {
return getOrders(user.id).then((orders) => {
console.log(user.name + ": " + orders.join(", "));
});
});
// Flat chain: return the promise so the next step waits.
getUser()
.then((user) => {
return getOrders(user.id).then((orders) => ({ user, orders }));
})
.then(({ user, orders }) => {
console.log(user.name + ": " + orders.join(", "));
});Branching is different. Calling .then twice on the same promise creates two branches. Both handlers receive the same original value. They do not form a sequence unless the second then is called on the promise returned by the first then.
const order = Promise.resolve("tea");
order.then((drink) => console.log("branch A: " + drink));
order.then((drink) => console.log("branch B: " + drink));
order
.then((drink) => drink + " + cake")
.then((combo) => console.log("chain: " + combo));| Shape | Meaning | When to use it |
|---|---|---|
p.then(a).then(b) | A sequence: b waits for a's returned value or promise. | Use for ordered steps. |
p.then(a); p.then(b); | Two branches: a and b both receive p's original value. | Use for independent observers. |
p.then(() => q.then(b)) | Nested chain: works, but can hide return paths. | Flatten when the outer caller must wait. |
Where you’ll use chains
LIVE LABChains are handy whenever step 2 needs step 1’s result: load a session, then use its user ID, then use the user data, then render. Each step returns the promise for the next piece of work so the whole function is waitable by its caller.
function loadProfile() {
return getSession()
.then((session) => getUser(session.userId))
.then((user) => getAvatar(user.avatarId))
.then((avatar) => renderProfile(avatar));
}Try building chains yourself. The lab below executes real promises in your browser and shows the value each step received, with milliseconds since you pressed Run.
Promise.resolve(1) .then((value) => { // Step 1: choose return value, promise, thenable, nothing, or throw. }) .then((value) => { // Step 2 receives what Step 1 returns or adopts. }) .then((value) => { // Step 3 receives what Step 2 returns or adopts. }) .catch((error) => { // A thrown error or rejection skips here. });- 0ms Press Run to build a real promise chain.
Change each handler, run again, and watch exactly what the next then receives. Thenables are adopted; no return means undefined; throw skips to catch.
Common chaining mistakes
DEBUG- “then changes the old promise.” It returns a new promise; the original promise keeps its own result.
- “Returning a promise makes a nested promise.” It does not. Promise resolution adopts the returned promise’s state.
- “Braces in an arrow function return the last expression.” They do not. Use
return. - “
.then(fn())means run fn later.” It callsfnimmediately and passes its result as the handler. - “An array of promises is flattened automatically.” No; arrays are ordinary values.
- “Starting an inner chain is enough.” If you do not return it, the caller cannot wait for it.
// Mistake 1: the function runs immediately.
start.then(doStep());
// Mistake 2: braces but no return.
start.then((value) => {
value + 1;
});
// Mistake 3: inner chain is started but not returned.
function saveThenEmail(user) {
saveUser(user).then(() => sendEmail(user));
}Another smell is the explicit-construction anti-pattern: wrapping a promise in new Promise only to resolve or reject with the same promise. Return the existing promise unless you are adapting a callback-style API.
// Avoid wrapping a promise just to forward it.
function loadMenuBad() {
return new Promise((resolve, reject) => {
getMenu().then(resolve, reject);
});
}
function loadMenuGood() {
return getMenu();
}Practice exercises
5 EXERCISESRun the steps in your head. What number does the final handler print?
Promise.resolve(2)
.then((value) => value + 3)
.then((value) => Promise.resolve(value * 2))
.then((value) => console.log(value));Promise.resolve(2)
.then((value) => value + 3)
.then((value) => Promise.resolve(value * 2))
.then((value) => console.log(value));2 becomes 5, the returned promise fulfills with 10, and the final handler logs 10.
What exact text prints? Then fix the code so it prints ADA.
Promise.resolve("Ada")
.then((name) => {
name.toUpperCase();
})
.then((name) => console.log(String(name)));Promise.resolve("Ada")
.then((name) => {
return name.toUpperCase();
})
.then((name) => console.log(name));Add return before name.toUpperCase(). Without it, the next handler receives undefined.
What two lines print, and why is this not a sequence?
const base = Promise.resolve("start");
base.then((value) => console.log("A " + value));
base.then((value) => console.log("B " + value));Promise.resolve("start")
.then((value) => "A " + value)
.then((value) => console.log(value));The original program has two branches. To make a sequence, call the second then on the promise returned by the first then.
What is the first log line? Explain why it appears before the promise handler.
function step() {
console.log("called now");
return "not a handler";
}
Promise.resolve("later")
.then(step())
.then((value) => console.log("next " + value));Promise.resolve("later")
.then((value) => console.log("handler " + value));Pass a function to then. Do not call a setup function unless it returns the actual handler you intend to pass.
Write saveThenEmail so callers can wait until both saving and emailing are done.
function saveUser(user) {
return Promise.resolve("saved " + user);
}
function sendEmail(user) {
return Promise.resolve("emailed " + user);
}
function saveThenEmail(user) {
return saveUser(user).then(() => sendEmail(user));
}
saveThenEmail("Ada").then((message) => console.log(message));function saveUser(user) {
return Promise.resolve("saved " + user);
}
function sendEmail(user) {
return Promise.resolve("emailed " + user);
}
function saveThenEmail(user) {
return saveUser(user).then(() => sendEmail(user));
}
saveThenEmail("Ada").then((message) => console.log(message));saveThenEmail returns the chain, and the first handler returns sendEmail(user), so callers can wait for the email result.
Check your understanding
8 QUESTIONSQuestion 1 of 8What does then return?
Choose an answer to see the explanation.
Question 2 of 8What prints?
Read the code, then predictPromise.resolve(1) .then((value) => value + 4) .then((value) => console.log(value));Choose an answer to see the explanation.
Question 3 of 8What prints when a handler returns a promise?
Read the code, then predictPromise.resolve("a") .then((value) => Promise.resolve(value + "b")) .then((value) => console.log(value));Choose an answer to see the explanation.
Question 4 of 8What prints when the arrow with braces forgets return?
Read the code, then predictPromise.resolve("Ada") .then((name) => { name.toUpperCase(); }) .then((value) => console.log(String(value)));Choose an answer to see the explanation.
Question 5 of 8What does the next then receive from a thenable?
Read the code, then predictPromise.resolve() .then(() => ({ then(resolve) { resolve(5); } })) .then((value) => console.log(value));Choose an answer to see the explanation.
Question 6 of 8What is true about
return [p1, p2]inside a then handler?Choose an answer to see the explanation.
Question 7 of 8What prints first?
Read the code, then predictfunction step() { console.log("called now"); return "not a handler"; } Promise.resolve("later") .then(step()) .then((value) => console.log(value));Choose an answer to see the explanation.
Question 8 of 8Which description is branching, not chaining?
Choose an answer to see the explanation.
Key takeaways
thenalways returns a new promise.- A handler’s returned plain value becomes the next fulfillment value.
- A returned promise or thenable is adopted, so chains flatten.
- No
returnmeans the next handler receivesundefined. - Calling
.thentwice on the same promise creates branches, not a sequence. - Throws and rejections skip to the next
catch; Error handling with promises is up next.
Promise chaining is sequencing async work by returning the next value or promise from each handler so the promise returned by then can carry the result forward.
Up next: Error handling with promises.