A single .then is enough when one asynchronous step stands alone. Real programs rarely work that way: step B needs the result of step A, and step C needs B's, so the value has to travel down the line. With callbacks that meant nesting each step inside the previous one — callback hell, the very shape the last lesson promised to dissolve.
Promises keep that promise through one simple property: .then itself returns a Promise. Because each step hands back another Promise, you do not nest — you chain. The calls line up left to right, and the result of one step flows naturally into the next. Reading the code top to bottom now matches the order the work actually happens, which is the whole reason chains exist.
flowchart LR P0["Promise.resolve(5)"] -->|".then(x => x*2)"| P1["resolves to 10"] P1 -->|".then(x => x+1)"| P2["resolves to 11"] style P0 fill:#3776ab,color:#fff style P2 fill:#1e7f3d,color:#fff
What .then actually returns
When you write promise.then(callback), the callback receives the resolved value, and the whole expression also produces a new Promise. That new Promise resolves to whatever your callback returns. So if the first Promise held 5 and your callback is x => x * 2, the .then expression resolves to 10, and the next .then in line receives that 10.
This is the whole secret of chaining. Each .then is a small machine that takes a value in, transforms it, and pushes the new value out to the next link. Nothing nests, because every link returns its own Promise for the following one to consume. A chain of three steps is just three .then calls in a row, each one feeding the next.
Promise.resolve(5)
.then((x) => x * 2)
.then((x) => x + 1)
.then((result) => console.log("result is " + result));
Returning a Promise from inside then
Sometimes the value you want to pass along is itself asynchronous — your step needs to start another Promise. You might expect to receive a Promise-of-a-Promise, an awkward nested object. You do not. When a .then callback returns a Promise, the chain unwraps it: the outer Promise adopts the inner one's state, and the next .then receives the inner value directly.
This flattening is what keeps chains readable. A step that kicks off more asynchronous work returns that Promise, and the chain picks up its result one link later, exactly as if the value had been plain. You never write .then(p => p.then(...)); you write two clean .then calls and let the engine join them for you.
flowchart TD
A[".then callback returns"] --> Q{"a Promise?"}
Q -->|"yes"| F["chain adopts it<br/>(unwrapped, flat)"]
Q -->|"no, a plain value"| V["chain resolves to that value"]
style F fill:#1e7f3d,color:#fff
style V fill:#3776ab,color:#fff
Promise.resolve(3)
.then((x) => Promise.resolve(x * 10))
.then((y) => console.log("got " + y));
How errors travel down a chain
A .then can take two handlers, one for success and one for failure, but far more often you attach a single .catch at the end. The reason is how rejections move. When a step throws, or returns a rejected Promise, the chain produces a rejection that skips every downstream .then until it meets a .catch.
So one .catch at the bottom guards the entire chain, no matter which link failed — a giant improvement over callbacks, where each level wanted its own if (err). And because .catch also returns a Promise, you can recover inside it by returning a fallback value, and the chain continues normally afterward, as if the failure never happened.
flowchart LR A["then #1 ok"] --> B["then #2 throws"] B -->|"rejection cascades"| C[".catch handles it"] D["then #3, #4..."] -.->|"skipped"| C style B fill:#b91c1c,color:#fff style C fill:#1e7f3d,color:#fff
Promise.resolve(5)
.then((x) => {
throw new Error("boom at " + x);
})
.then((x) => console.log("this never runs"))
.catch((err) => console.log("caught: " + err.message));
What does this print? A .then expression is itself a Promise, even though its value arrives later.
const p = Promise.resolve(1).then((x) => x + 1); console.log(typeof p); console.log(p instanceof Promise);
.then always returns a new Promise.
So typeof is "object" and instanceof Promise is true.
Write addOneChain(n) that returns a Promise resolving to ((n + 1) * 2), built by chaining two .then calls on Promise.resolve(n) — first add one, then multiply by two.
function addOneChain(n) {
// chain two .then calls on Promise.resolve(n)
}
Start from Promise.resolve(n) and chain .then((x) => x + 1) then .then((x) => x * 2).
For n = 3: 3 + 1 = 4, then 4 * 2 = 8.
function addOneChain(n) {
return Promise.resolve(n).then((x) => x + 1).then((x) => x * 2);
}
This chain is meant to double n, then add one. But the first .then uses braces and forgets to return, so it resolves to undefined and the second step receives undefined (and undefined + 1 is NaN). Add the missing return so the value flows through.
function doubleThenAdd(n) {
return Promise.resolve(n)
.then((x) => { x * 2; })
.then((x) => x + 1);
}
A .then callback with braces needs an explicit return.
Change { x * 2; } to { return x * 2; }, or drop the braces: (x) => x * 2.
function doubleThenAdd(n) {
return Promise.resolve(n)
.then((x) => { return x * 2; })
.then((x) => x + 1);
}
What does this print? The chain is a Promise, and you consume it the same way as any other.
const chain = Promise.resolve(3).then((x) => x * 2); console.log(chain instanceof Promise); console.log(typeof chain.then);
A .then chain is still a Promise.
So instanceof Promise is true, and chain.then is a function.
Using the provided fetchProfile(id) (which resolves to a user object), write greetAfter(id) that chains two .then calls: extract the user's name, then return the string "Hello, " + name. Return the chain so callers get the greeting.
function fetchProfile(id) {
return Promise.resolve({ name: "Ada", age: 30 });
}
function greetAfter(id) {
// chain fetchProfile(id) -> .then(extract name) -> .then(build greeting)
}
First .then receives the user object; return user.name from it.
Second .then receives the name; return "Hello, " + name.
function fetchProfile(id) {
return Promise.resolve({ name: "Ada", age: 30 });
}
function greetAfter(id) {
return fetchProfile(id)
.then((user) => user.name)
.then((name) => "Hello, " + name);
}
Recap
.thenreturns a new Promise that resolves to whatever the callback returns — that is why chains work at all.- Each link receives the previous link's value, so a sequence of dependent steps reads left to right with no nesting.
- Returning a Promise from inside
.thenunwraps it; the chain adopts the inner state and passes the inner value onward. - A rejection skips downstream
.thencalls until a.catch, so one.catchcan guard an entire chain. - A
.thenthat uses braces but forgetsreturnresolves to undefined — the most common chaining bug.
Next comes a tool for the opposite situation: many tasks that do NOT depend on each other, which you want running at the same time with Promise.all.