JavaScript Promise Chaining: A Complete Guide

Promise chaining links multiple .then() calls into a flat pipeline instead of nested callbacks. Learn how values and errors flow through a chain.

8 min read

JavaScript promise chaining is the pattern of linking multiple .then() calls so that each asynchronous step starts after the previous one finishes. Instead of nesting callbacks inside callbacks, you create a flat pipeline where each step receives the result of the step before it.

A Promise represents a value that is not available yet. Calling then on a promise registers a callback that runs when the promise resolves. The key detail that makes chaining work is that a then call itself returns a new promise, so you can chain another then right after it.

javascriptjavascript
function getUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: "Taylor" }), 500);
  });
}
 
function getPosts(user) {
  return new Promise((resolve) => {
    setTimeout(() => resolve([`${user.name}'s post 1`]), 500);
  });
}

Both helpers simulate a real API call by resolving after a short delay instead of returning a value right away. Chain them together and each step waits for the one before it to finish.

javascriptjavascript
getUser(1)
  .then((user) => {
    console.log("Got user:", user.name);
    return getPosts(user);
  })
  .then((posts) => {
    console.log("Got posts:", posts);
  });

Running this prints the user first, then the posts, roughly one second later once both delays have passed. Nothing about the syntax forces that order. The order comes entirely from the second then waiting for the promise returned by the first.

texttext
Got user: Taylor
Got posts: ["Taylor's post 1"]

The return value of one callback becomes the input of the next. This flat structure replaces what would otherwise be deeply nested callbacks, one inside another.

How then returns a new promise

Every then call creates and returns a brand-new promise. What that new promise resolves to depends on what the callback returns.

Promise chain value flow

The diagram shows the value moving forward one step at a time. Each callback receives the resolved value of the promise before it, and the value it returns becomes the resolved value of the next promise in line.

javascriptjavascript
Promise.resolve(10)
  .then((num) => {
    console.log(num); // 10
    return num * 2;
  })
  .then((num) => {
    console.log(num); // 20
    return num + 5;
  })
  .then((num) => {
    console.log(num); // 25
  });

Three then calls, three values flowing forward, no callbacks nested inside other callbacks. Each step transforms the number and passes it along, and the comments show exactly what each step logs.

Returning a promise from then

When a then callback returns a promise instead of a plain value, the chain does not move on right away. It waits for that inner promise to settle first, then passes the resolved value to the next step.

javascriptjavascript
function delayValue(value, ms) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}

This helper stands in for any operation that finishes after a delay, such as a network request. Chaining two calls to it shows the waiting behavior directly.

javascriptjavascript
delayValue("step 1", 400)
  .then((result) => {
    console.log(result);
    return delayValue("step 2", 400);
  })
  .then((result) => {
    console.log(result);
  });

The second value only logs after its own 400ms delay finishes, not immediately after the first one. The chain pauses at each step until the returned promise settles, so the total time is the sum of both delays rather than the longer of the two.

texttext
step 1   (after 400ms)
step 2   (after another 400ms)

Returning a promise inside then is the standard way to sequence async operations without nesting. If you are new to how JavaScript schedules delayed callbacks, see JavaScript setTimeout Behavior: Complete Guide for the timer mechanics used in these examples.

Error handling with .catch()

A .catch() placed at the end of a chain catches any rejection from any then above it. The error skips all remaining then handlers and jumps straight to the nearest catch.

javascriptjavascript
function fetchData(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id < 0) {
        reject(new Error("Invalid ID"));
      } else {
        resolve({ id, data: "Some data" });
      }
    }, 500);
  });
}

This helper rejects when the ID is negative and resolves otherwise, which lets the next example show both paths through a chain.

javascriptjavascript
fetchData(-1)
  .then((result) => {
    console.log("Processing:", result.data);
    return result.data.toUpperCase();
  })
  .then((processed) => {
    console.log("Done:", processed);
  })
  .catch((error) => {
    console.error("Failed:", error.message);
  });

Because the call rejects with a negative ID, neither then callback runs at all. The error skips straight past both of them and lands directly in the catch handler at the end of the chain.

texttext
Failed: Invalid ID

You can also place a catch in the middle of a chain to recover from an error and let the chain continue, instead of ending it.

javascriptjavascript
fetchData(42)
  .then((result) => {
    throw new Error("Something went wrong during processing");
  })
  .catch((error) => {
    console.warn("Recovered from:", error.message);
    return { id: 42, data: "Fallback data" };
  })
  .then((result) => {
    console.log("Still got:", result.data);
  });

The catch here returns a fallback value instead of re-throwing, so the chain keeps going as if that step had succeeded.

texttext
Recovered from: Something went wrong during processing
Still got: Fallback data

This is the promise equivalent of a try/catch block that recovers from an error and keeps running instead of stopping the whole program.

Finally: run code regardless of outcome

A .finally() callback runs whether the chain resolves or rejects. It does not receive the result or the error, and whatever it returns does not affect the value passed down the chain.

javascriptjavascript
function loadData() {
  return fetchData(1)
    .then((result) => console.log("Data:", result.data))
    .catch((error) => console.error("Error:", error.message))
    .finally(() => console.log("Loading finished"));
}
 
loadData();

Since fetchData(1) resolves, the then callback logs the data first and the catch callback is skipped entirely, but finally still runs afterward.

texttext
Data: Some data
Loading finished

Finally is ideal for cleanup work that must happen either way: hiding a loading spinner, closing a connection, or resetting UI state. If fetchData(1) had rejected instead, the catch callback would have logged the error, and finally would still run afterward and print the same "Loading finished" message. That is the point of finally: it does not care which path the chain took, only that the chain is done.

The sequence matters: catch placement

Where you put a catch in a chain changes which errors it actually handles. Each catch only sees errors from the steps above it, not the ones after it.

javascriptjavascript
Promise.resolve("start")
  .then((val) => {
    console.log("Step 1:", val);
    throw new Error("Step 1 failed");
  })
  .catch((err) => {
    console.log("Caught:", err.message);
    return "recovered";
  })
  .then((val) => {
    console.log("Step 2:", val);
    throw new Error("Step 2 failed");
  })
  .catch((err) => console.log("Caught:", err.message));

The first catch handles the error thrown in Step 1 and returns a recovered value, so Step 2 runs normally before throwing its own error, which the second catch handles separately.

texttext
Step 1: start
Caught: Step 1 failed
Step 2: recovered
Caught: Step 2 failed

A single catch at the very end would catch everything but give you less control over recovery. Multiple catch calls let you handle specific failures and keep the rest of the chain running.

This placement rule is the same reason a .catch() in the middle of a chain, as shown in the error handling section above, does not affect errors thrown by steps that come after it. Each catch is a checkpoint for the chain above it, not a safety net for the entire chain.

Common promise chaining mistakes

Forgetting to return the promise. If a then callback calls an async function but does not return it, the next then does not wait for it and receives undefined instead.

javascriptjavascript
// Mistake: getPosts returns a promise, but we do not return it
getUser(1).then((user) => {
  getPosts(user); // No return!
}).then((posts) => {
  console.log(posts); // undefined
});
 
// Correct: return the promise
getUser(1)
  .then((user) => getPosts(user))
  .then((posts) => console.log(posts)); // Array of posts

The mistake version calls getPosts but throws away its result, so the second then has nothing to wait for. The corrected version returns getPosts directly, which keeps the chain connected.

Nesting then instead of chaining. Calling then inside another then recreates the same callback pyramid that chaining is meant to avoid.

javascriptjavascript
// Mistake: nested then
getUser(1).then((user) => {
  getPosts(user).then((posts) => {
    console.log(posts);
  });
});
 
// Correct: flat chain
getUser(1)
  .then((user) => getPosts(user))
  .then((posts) => console.log(posts));

Both versions produce the same result, but the flat chain reads top to bottom instead of growing deeper with every added step.

Assuming order when multiple promises are created at once. If you create several promises at the same time and chain them, all of those promises start running immediately, not one after another. To run work in a strict order, create each promise inside the then callback of the one before it. For the async/await equivalent of this pattern, see JavaScript Async Await Guide: Complete Tutorial.

Swallowing errors silently. A catch handler that does nothing with the error hides real bugs from you. At minimum, log the error, or re-throw it if the caller is the one who should actually handle it.

Rune AI

Rune AI

Key Insights

  • Every .then() returns a new promise, enabling flat chaining without nesting.
  • Whatever you return from a .then() callback becomes the resolved value of the next promise.
  • A .catch() handles rejections from any preceding step in the chain.
  • .finally() runs regardless of success or failure and does not receive the result.
  • Returning a promise inside .then() flattens it automatically, preventing nested chains.
RunePowered by Rune AI

Frequently Asked Questions

Does .then() always return a promise?

Yes. Every .then() call returns a new promise, even if your callback returns a plain value. That value is automatically wrapped in a resolved promise so the chain can continue.

What is the difference between .then() and .catch()?

.then() handles the fulfilled case. .catch() is shorthand for .then(undefined, errorHandler) and handles only the rejected case. A .catch() placed in the middle of a chain catches errors from earlier steps but not from later ones.

Can I have multiple .catch() calls in one chain?

Yes. A .catch() in the middle of a chain handles errors above it and then passes control to the next .then(). A .catch() at the end catches any error that was not handled earlier.

Conclusion

Promise chaining is the pattern that makes asynchronous code in JavaScript readable. Every .then() returns a new promise, values flow forward, and errors bubble to the nearest .catch(). Once you understand that a chain is just a series of transformations, you can structure any async workflow as a clean pipeline.