Module 5 · Asynchronous JavaScript ⏱ 19 min

Promise Chaining

By the end of this lesson you will be able to:
  • Explain why .then returns a new Promise and how a value flows through a chain
  • Sequence dependent asynchronous steps by returning a value from one .then into the next
  • Trace how a rejection propagates down a chain until a .catch catches it

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
Each .then returns a new Promise. The value from one link becomes the input to the next.

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.

A chain: each .then receives the previous value and returns the next. Press Run.
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
Returning a Promise from .then flattens it — the chain adopts the inner state, no nesting.
Returning a Promise from .then: the chain unwraps it and passes the inner value on.
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
A rejection cascades past later .then calls until a .catch handles it.
A throw inside a .then rejects the chain; the next .then is skipped and .catch runs.
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));
Exercise

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);
Exercise

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)
}
Exercise

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);
}
Exercise

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);
Exercise

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)
}

Recap

  • .then returns 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 .then unwraps it; the chain adopts the inner state and passes the inner value onward.
  • A rejection skips downstream .then calls until a .catch, so one .catch can guard an entire chain.
  • A .then that uses braces but forgets return resolves 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.

Checkpoint quiz

In Promise.resolve(5).then(x => x * 2).then(x => x + 1), what value does the final Promise resolve to?

A .then callback computes a value but forgets to return it. What does the next .then receive?

Go deeper — technical resources