cf.completefrontendCode editorOpen lab
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, chaining should feel predictable
  • 01
    Return the next batonExplain what the next then receives when a handler returns a value, a promise, a thenable, or nothing.
  • 02
    Read flattening correctlyKnow why returned promises and thenables are adopted instead of being nested promises.
  • 03
    Avoid 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.

Promise chaining, in one sentence

A promise chain runs async steps in order because every then returns a new promise that adopts whatever its handler returns.

Real-life analogyA chain is a relay race

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 then handler receives a value
In real life: Each runner hands on a baton
In JavaScript: Each handler’s return becomes the next value
In real life: Dropping the baton confuses the next runner
In JavaScript: Forgetting return passes undefined

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 THROUGH

then always returns a new promise. That new promise is fulfilled or rejected based on what your handler does. The everyday cases are straightforward:

What a then handler does to the next promise
Handler behaviorNew promise becomesNext then receives
Returns a plain valueFulfilled with that valueThat value
Returns a promiseAdopts that promise’s stateThe fulfilled value, or skips to catch if rejected
Returns a thenableAdopts the thenable’s resultWhatever the thenable resolves with
Returns nothingFulfilled with undefinedundefined
ThrowsRejected with the thrown errorThe 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 a chain
Step 0 of 12Ready
Your turn: follow the blue line

Follow the baton through three promise handlers. Change step 2 and predict what step 3 receives.

Running in
  1. script
Next: line 5
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Step 2 behavior
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.

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

INTERACTIVE

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

Real-life analogyA receipt with an order number

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 then method
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.

Promises and thenables both flatten to valuesPop 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 above calls resolve(5) and then tries resolve(99). Only the first resolve counts. If a thenable throws before resolving, the adopting promise rejects:

A throwing thenable rejectsPop 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));

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 IT
Real-life analogyA token leads to the food

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

What does the next then receive?
  • 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

Sort each handler result by the value received by the next fulfillment handler.

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

Nested, flat, or branched?

COMPARE

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

Nested pyramid vs 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 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.

Branches share the same original 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));
Three similar shapes
ShapeMeaningWhen 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 LAB

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

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

Chain builder: choose each handler's return
Chain builder sourcePop out in the code editor (opens in a new tab)JavaScript
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.  });
Timelineidle
  1. 0ms Press Run to build a real promise chain.
Try it yourself

Change each handler, run again, and watch exactly what the next then receives. Thenables are adopted; no return means undefined; throw skips to catch.

This is not a simulation: it builds a real Promise chain in the page. Reset clears pending timers and restores the initial choices.

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 calls fn immediately 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.
Three bugs that break chainsJavaScript
// 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 an existing promisePop 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 final 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, and 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 immediate call

        What is the first log line? Explain why it appears before the promise handler.

        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 callers can wait until both saving and emailing are done.

          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 the arrow with braces forgets 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 receive from 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 description is branching, not chaining?

              Choose an answer to see the explanation.

            Key takeaways

            • then always 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 return means the next handler receives undefined.
            • Calling .then twice on the same promise creates branches, not a sequence.
            • Throws and rejections skip to the next catch; Error handling with promises is up next.
            Final definition

            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.

            CompleteFrontend Clear concepts. Working examples.