completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

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.

By the end, you can tell what each step of a chain gets
  • 01
    Hand on the next valueSay what the next then gets when a handler returns a value, a promise, a thenable, or nothing.
  • 02
    Understand flatteningKnow why a returned promise is waited for, so the next step gets its value and not a promise inside a promise.
  • 03
    Follow the error pathExplain how a thrown error skips to catch, how catch puts the chain back on track, and why finally lets 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.

Promise chaining, in one sentence

A promise chain runs steps in order because every then makes a new promise, and that new promise gets whatever its handler returns.

Real-life analogyA chain is a relay race

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 then handler 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 return hands on undefined

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 THROUGH

then 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 a then handler does to the next promise
What the handler doesWhat the new promise becomesWhat 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.
Three cards in a row. The first promise holds start, from line 5. An arrow leads to the step 1 function, which returns on line 8. An arrow leads to the new promise, which holds start arrow step 1. Step 2 gets this value."start"first promiseline 5step 1returnsline 8"start → step 1"new promisestep 2 gets this
Step 1 gets "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 a chain
Step 0 of 12Ready
Your turn: follow the blue line

Follow the baton through three handlers. Change what step 2 does, and guess what step 3 gets.

Running in
  1. script
Next: line 5
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));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
What step 2 does
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Two things to notice:

  • The second then does 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

INTERACTIVE

A 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.

Real-life analogyA receipt with an order number

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 then method
In real life: The counter checks if the order is ready
In JavaScript: The chain calls the object's then method
A promise and a thenable both give the next step 5Pop out in the code editor (opens in a new tab)JavaScript
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:

A thenable that throws makes the chain failPop out in the code editor (opens in a new tab)JavaScript
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 IT

Flattening means this: when a handler returns a promise, the next handler gets the value inside it, not a promise inside a promise.

Real-life analogyA token leads to the food

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
The handler returns Promise.resolve(5), drawn as a cart called promise holding the card 5. An arrow labelled waits leads to a plain card 5. The next then gets 5, not a promise..then(() => Promise.resolve(5))5promisereturned promisewaits5next then gets 5
The handler returns a promise, not a plain value. The chain waits for that promise and gives its value, 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.

What does the next then get?
  • return 5
  • return Promise.resolve(5)
  • return { then(resolve) { resolve(5); } }
  • no return
  • return [p1, p2]
  • .then(step())
Try it yourself
0 of 6 correct

Put each card in the group that says what the next then handler gets.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Nested, flat, or branched?

COMPARE

Returning 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 then waits for it.
Nested steps vs a flat chainPop out in the code editor (opens in a new tab)JavaScript
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.

Two branches get the same valuePop out in the code editor (opens in a new tab)JavaScript
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));
Left, two branches: the order promise holds tea. Two arrows lead to branch A and branch B, and both get tea. Right, one chain: tea leads to the plus cake handler, which leads to tea plus cake, the value the next then gets.two branches"tea"orderbranch Agets "tea"branch Bgets "tea"one chain"tea"+ cake"tea + cake"next then gets this
Lines 3 and 4 both call 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.

Three shapes of promise code that look alike
When you writeWhat it meansWhen 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 PATH

If a handler inside then throws an error, the new promise from that then is rejected. Then:

  • The next then handlers are skipped.
  • The next catch gets the error.
catch can put the chain back on trackPop out in the code editor (opens in a new tab)JavaScript
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:

  1. The first handler throws an error on line 3.
  2. catch gets the error, prints it, and returns a new value, "use wishlist". Because catch returned a value, the chain is back on the success path.
  3. finally runs to clean up. It gets no value and can't change the value. Even its return "ignored" is thrown away.
  4. The last handler gets "use wishlist".
Four handlers in a row. then throws out of stock. catch recovers and returns use wishlist. finally does cleanup and its return, ignored, is dropped. The last then gets use wishlist.thenthrowsout of stockcatchrecovers"use wishlist"finallycleanup"ignored" droppedthengets"use wishlist"
The throw on line 3 jumps to 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 LAB

Chains 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.

A real chain returns every stepPop out in the code editor (opens in a new tab)JavaScript
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.

Chain builder: choose what each step returns
The chain the lab buildsPop out in the code editor (opens in a new tab)JavaScript
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.  });
What happenedidle
  1. 0ms Press Run to build a real promise chain.
Try it yourself

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.

This builds a real promise chain in the page. Reset stops any waiting steps and puts the first choices back.

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()) runs fn later.” No. The round brackets call fn right away, and then gets whatever fn returned.
  • “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.
Three bugs that break chainsJavaScript
// 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 when start is done.
  • Lines 5 to 7: the handler works out value + 1 but never returns it.
  • Line 11: the inner chain starts, but saveThenEmail doesn'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.

Return the promise you already havePop out in the code editor (opens in a new tab)JavaScript
// 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 EXERCISES
Exercise 1 · Warm-upPredict a returned value

Run the steps in your head. What number does the last handler print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
Promise.resolve(2)
  .then((value) => value + 3)
  .then((value) => Promise.resolve(value * 2))
  .then((value) => console.log(value));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the missing return

    What exact text prints? Then fix the code so it prints ADA.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    Promise.resolve("Ada")
      .then((name) => {
        name.toUpperCase();
      })
      .then((name) => console.log(String(name)));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeBranch or sequence?

      What two lines print? Why is this not a sequence?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const base = Promise.resolve("start");
      base.then((value) => console.log("A " + value));
      base.then((value) => console.log("B " + value));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeSpot the call that runs right away

        What is the first line printed? Why does it print before any promise handler runs?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function step() {
          console.log("called now");
          return "not a handler";
        }
        
        Promise.resolve("later")
          .then(step())
          .then((value) => console.log("next " + value));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeReturn the inner chain

          Write saveThenEmail so the code that calls it can wait until the user is saved and the email is sent.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          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));

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Promise chaining quiz · 8 questionsScore: first tries count
            1. Question 1 of 8What does then return?

              Choose an answer to see the explanation.

            2. Question 2 of 8What prints?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              Promise.resolve(1)
                .then((value) => value + 4)
                .then((value) => console.log(value));

              Choose an answer to see the explanation.

            3. Question 3 of 8What prints when a handler returns a promise?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              Promise.resolve("a")
                .then((value) => Promise.resolve(value + "b"))
                .then((value) => console.log(value));

              Choose an answer to see the explanation.

            4. Question 4 of 8What prints when an arrow function with curly braces has no return?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              Promise.resolve("Ada")
                .then((name) => {
                  name.toUpperCase();
                })
                .then((value) => console.log(String(value)));

              Choose an answer to see the explanation.

            5. Question 5 of 8What does the next then get when a handler returns a thenable?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              Promise.resolve()
                .then(() => ({ then(resolve) { resolve(5); } }))
                .then((value) => console.log(value));

              Choose an answer to see the explanation.

            6. Question 6 of 8What is true about return [p1, p2] inside a then handler?

              Choose an answer to see the explanation.

            7. Question 7 of 8What prints first?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function 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.

            8. Question 8 of 8Which code makes two branches, not a chain?

              Choose an answer to see the explanation.

            Key takeaways

            • then always 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 gets undefined.
            • Calling .then twice 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.
            Final definition

            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.

            CompleteFrontend Clear concepts. Working examples.