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.
- 01Hand on the next valueSay what the next
thengets when a handler returns a value, a promise, a thenable, or nothing. - 02Understand flatteningKnow why a returned promise is waited for, so the next step gets its value and not a promise inside a promise.
- 03Follow the error pathExplain how a thrown error skips to
catch, howcatchputs the chain back on track, and whyfinallylets the value pass.
Chains pass the baton
A promise chain is a row of .then calls, one after another. Each .then holds a handler: a function that runs when the step before it is done.
The handler does two things:
- It gets the value from the step before it.
- It returns the value for the next step.
The key word is return. The value a handler returns is the value the next handler gets. If the handler returns a promise, the chain waits for that promise first.
A promise chain runs steps in order because every then makes a new promise, and that new promise gets whatever its handler returns.
Picture three runners in a relay race. Runner 1 gets a baton that says "start". Runner 1 writes on it and hands it to runner 2.
Runner 2 can do one of three things:
- Hand the baton straight on.
- Wait a moment, then hand it on.
- Forget to hand anything on.
Runner 3 gets whatever runner 2 handed on.
- In real life: The first runner starts with the baton
- In JavaScript: The first promise has a value
- In real life: Each runner takes the baton
- In JavaScript: Each
thenhandler gets a value - In real life: Each runner hands a baton to the next runner
- In JavaScript: What a handler returns is the next value
- In real life: A runner who drops the baton hands on nothing
- In JavaScript: A handler without
returnhands onundefined
The Promises lesson showed then, catch, and finally on one promise. This lesson shows what happens when you join several steps together, and what each handler's return does. Errors get a short look here. The next lesson, Error handling with promises, covers them fully.
Returning values & promises
STEP THROUGHthen always gives back a new promise. What your handler does decides what happens to that new promise.
How to read this table: each row is one thing a handler can do. The other columns say what happens to the new promise and what the next then gets.
| What the handler does | What the new promise becomes | What the next then gets |
|---|---|---|
It returns a plain value, like 5. | It is fulfilled with that value. | It gets that value. |
| It returns a promise. | It waits for that promise and ends up the same way. | It gets the promise's value. If that promise fails, the next catch runs instead. |
It returns a thenable (an object with a then method). | It waits for the thenable and takes its result. | It gets the value the thenable gives. |
| It returns nothing. | It is fulfilled with undefined. | It gets undefined. |
| It throws an error. | It is rejected with that error. | It is skipped. The next catch gets the error. |
"start". What it returns on line 8 becomes the value of the new promise, so step 2 gets "start → step 1".Now step through a real chain. Pick what step 2 does, then press Replay. Before you look, guess what line 15 prints: what does step 3 get?
Follow the baton through three handlers. Change what step 2 does, and guess what step 3 gets.
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));Two things to notice:
- The second
thendoes not change the first promise. It makes a new promise, and that new promise gets what step 2 returns. - When step 2 returns a promise, step 3 does not get a promise. The chain waits for it and hands step 3 the value inside.
Thenables: promise-shaped values
INTERACTIVEA thenable is any object (or function) that has a method called then. It is not a real promise. But a chain treats it like one: it calls the object's then method and waits for the value.
This lets older code libraries, or your own objects, join a promise chain.
A café counter gives out its own order tokens. It also takes a printed receipt with an order number. The receipt is not a token, but the counter can still check it and say "ready" or "sold out".
- In real life: The counter takes its own order token
- In JavaScript: A chain takes a real Promise
- In real life: It also takes a printed receipt with a number
- In JavaScript: A chain takes an object with a
thenmethod - In real life: The counter checks if the order is ready
- In JavaScript: The chain calls the object's
thenmethod
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 on lines 1 to 6 calls resolve(5), then resolve(99). Only the first call counts, so the chain gets 5.
If a thenable throws an error before it calls resolve, the chain fails instead:
const thenable = {
then(resolve) {
throw new Error("broken ticket");
resolve(5);
},
};
Promise.resolve()
.then(() => thenable)
.catch((error) => console.log(error.message));One more detail. If p is a real promise, Promise.resolve(p) gives back p itself. But new Promise((resolve) => resolve(p)) makes a different promise that waits for p. Both end the same way, but they are two different objects.
Flattening keeps chains flat
SORT ITFlattening means this: when a handler returns a promise, the next handler gets the value inside it, not a promise inside a promise.
At a café, you may get an order token instead of food. When the order is ready, you hand in the token and get the food. You don't get another token.
- In real life: The counter hands you food
- In JavaScript: A handler returns a plain value
- In real life: The counter hands you an order token
- In JavaScript: A handler returns a promise
- In real life: You wait until the token's order is ready
- In JavaScript: The chain waits for the returned promise
- In real life: You collect food, not another token
- In JavaScript: The next then gets the value, not a promise
5, to the next then.Flattening only happens for the one promise or thenable that a handler returns. It does not look inside other values. An array of promises is just an array, so the next handler gets the array. Waiting for many promises at once is covered in the Promise combinators lesson.
return 5return Promise.resolve(5)return { then(resolve) { resolve(5); } }no returnreturn [p1, p2].then(step())
Put each card in the group that says what the next then handler gets.
Nested, flat, or branched?
COMPAREReturning a promise lets you write steps from top to bottom. You don't need to put each new step inside the step before it, where the code moves further right every time.
The code below does the same job twice:
- The nested version (lines 9 to 13) works, but every new step adds another level of indent.
- The flat version (lines 16 to 22) returns the inner promise, so the next
thenwaits for it.
function getUser() {
return Promise.resolve({ id: 7, name: "Ada" });
}
function getOrders(userId) {
return Promise.resolve(["tea", "cake"]);
}
// Nested: it works, but the code moves further right each step.
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(", "));
});A branch is something different. If you call .then twice on the same promise, you get two branches. Both handlers get the same value. Neither waits for the other.
You get a sequence only when the second then is called on the promise that the first then gave back.
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));then on order, so both get "tea". Lines 6 to 8 are a chain: the second handler gets what the first one returned.How to read this table: each row is one shape of code. The columns say what it means and when to use it.
| When you write | What it means | When to use it |
|---|---|---|
p.then(a).then(b) | A sequence: b waits for what a returns. | Use it for steps that must happen in order. |
p.then(a); p.then(b); | Two branches: a and b both get the value of p. | Use it when two pieces of code each need the same value. |
p.then(() => q.then(b)) | A chain inside a chain: it works, but it is easy to forget a return. | Make it flat when the outer code must wait for it. |
When a handler throws
FOLLOW THE PATHIf a handler inside then throws an error, the new promise from that then is rejected. Then:
- The next
thenhandlers are skipped. - The next
catchgets the error.
Promise.resolve("cart")
.then(() => {
throw new Error("out of stock");
})
.catch((error) => {
console.log("caught: " + error.message);
return "use wishlist";
})
.finally(() => {
console.log("cleanup");
return "ignored";
})
.then((value) => console.log("next: " + value));Here is what happens, step by step:
- The first handler throws an error on line 3.
catchgets the error, prints it, and returns a new value,"use wishlist". Becausecatchreturned a value, the chain is back on the success path.finallyruns to clean up. It gets no value and can't change the value. Even itsreturn "ignored"is thrown away.- The last handler gets
"use wishlist".
catch. catch returns "use wishlist", finally lets it pass, and the last then gets it.A good rule: put clean-up work in finally, and put "what do we do instead?" decisions in catch.
Where you’ll use chains
LIVE LABChains help when step 2 needs the result of step 1. For example: load a session, then use its user ID to load the user, then make a greeting.
Each step returns the promise for the next job. That way, the code that calls your function can wait for the whole thing to finish.
function loadProfile() {
return Promise.resolve({ userId: 3 })
.then((session) => Promise.resolve({ id: session.userId, name: "Asha" }))
.then((user) => "Hello " + user.name);
}
loadProfile().then((message) => console.log(message));- Line 3: the first handler turns the session into a user object.
- Line 4: the next handler turns that user into
"Hello Asha". - Line 2 returns the whole chain, so on line 7 the caller can add its own last step, which prints the greeting.
Try the lab below. Change what one step returns and watch what the next step gets.
Promise.resolve(1) .then((value) => { // Step 1: return a value, a promise, a thenable, nothing, or throw. }) .then((value) => { // Step 2 gets what Step 1 handed on. }) .then((value) => { // Step 3 gets what Step 2 handed on. }) .catch((error) => { // A thrown error or a failed promise jumps here. });- 0ms Press Run to build a real promise chain.
Change what each step does, press Run, and watch what the next then gets. A thenable is waited for, no return hands on undefined, and throw jumps to catch.
Common chaining mistakes
DEBUG- “then changes the old promise.” No. It makes a new promise. The old promise keeps its own value.
- “Returning a promise makes a promise inside a promise.” No. The chain waits for the returned promise and passes on its value.
- “An arrow function with curly braces returns its last line.” No. Inside curly braces you must write
return. - “
.then(fn())runsfnlater.” No. The round brackets callfnright away, andthengets whateverfnreturned. - “An array of promises is unwrapped too.” No. An array is a normal value, so the next step gets the array.
- “Starting an inner chain is enough.” No. If you don't return it, the code that called your function can't wait for it.
// Mistake 1: the function runs immediately.
start.then(doStep());
// Mistake 2: curly braces but no return.
start.then((value) => {
value + 1;
});
// Mistake 3: the inner chain starts but is not returned.
function saveThenEmail(user) {
saveUser(user).then(() => sendEmail(user));
}- Line 2:
doStep()runs right away, not whenstartis done. - Lines 5 to 7: the handler works out
value + 1but never returns it. - Line 11: the inner chain starts, but
saveThenEmaildoesn't return it.
One more mistake: wrapping a promise you already have inside new Promise, just to pass on its result. It adds extra code and nothing else. Return the promise you have. Use new Promise only to turn old callback-style code into a promise.
// 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 last 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 has the value 10, so the last handler prints 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 gets undefined.
What two lines print? 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 first program has two branches. To make a sequence, call the second then on the promise that the first then gives back.
What is the first line printed? Why does it print before any promise handler runs?
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));Give then a function. Don't call a function inside the brackets of then, unless that call returns the handler you want.
Write saveThenEmail so the code that calls it can wait until the user is saved and the email is sent.
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 the caller can wait for the email to be sent.
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 an arrow function with curly braces has no 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 get when a handler returns 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 code makes two branches, not a chain?
Choose an answer to see the explanation.
Key takeaways
thenalways gives back a new promise.- When a handler returns a plain value, the next handler gets that value.
- When a handler returns a promise or thenable, the chain waits and passes on its value.
- With no
return, the next handler getsundefined. - Calling
.thentwice on the same promise makes two branches, not a sequence. - A thrown error skips to the next
catch. Error handling with promises is up next.
Promise chaining means running steps in order. Each handler returns the next value or promise, and the new promise from then carries it to the next step.
Up next: Error handling with promises.