W3docs

JavaScript Promise Chaining

In today's digital age, JavaScript stands as a cornerstone of web development, empowering developers to create dynamic, responsive, and highly interactive web

Promise chaining lets you run asynchronous operations one after another, where each step starts only after the previous one finishes. You attach a sequence of .then() handlers to a promise, and each handler receives the result of the step before it.

Before promises, sequencing async work meant nesting callbacks inside callbacks — the infamous "callback hell" or "pyramid of doom":

queryDatabase('users', (users) => {
  queryDatabase('posts', (posts) => {
    queryDatabase('comments', (comments) => {
      // deeply nested, hard to read, error handling duplicated everywhere
    });
  });
});

Chaining flattens that pyramid into a readable, top-to-bottom sequence with a single place to handle errors. This page covers how chaining actually works, the most common bug (a missing return), error recovery, and cleanup. For the related syntax that builds on top of promises, see JavaScript: async/await.

How Chaining Works: Every .then() Returns a New Promise

This is the core mechanic, and everything else follows from it. .then() does not return the original promise — it returns a brand-new promise. What that new promise resolves to depends on what your handler returns:

  • Return a plain value → the next .then() receives that value.
  • Return a promise → the chain waits for it to settle, and the next .then() receives its resolved value (not the promise itself).
  • Return nothing → the next .then() receives undefined.
  • Throw an error → the chain skips ahead to the nearest .catch().

Because each .then() returns a new promise, you can keep attaching .then() calls and pass a value down the line:

javascript— editable

The powerful case is returning a promise from a handler. The chain pauses until that promise resolves before moving on, which is exactly how you sequence dependent async operations:

Basic Promise Chaining

Consider the scenario where you need to query a database, then use the result of that query to make another query. Each .then() returns the promise from the next query, so the chain waits for one query to finish before starting the next:

javascript— editable

The #1 Bug: Forgetting return (the "detached chain")

This is the single most common promise-chaining mistake. If you start an async operation inside a .then() but forget to return its promise, the chain does not wait for it — the result is lost and the next .then() runs immediately with undefined. The inner promise becomes a "detached" chain running on its own.

In the broken version below, queryDatabase('posts') is called but its promise isn't returned, so the second .then() logs undefined instead of the posts:

javascript— editable

Adding return reconnects the chain. Now the second .then() waits for the posts query and receives its result:

javascript— editable

Tip: arrow functions with an expression body return automatically — .then(r => queryDatabase(r)) returns the promise, but .then(r => { queryDatabase(r); }) (with braces) does not.

Error Handling in Chains

A single .catch() at the end of the chain handles any error thrown — or any promise rejected — at any earlier step. When something fails, the chain skips every remaining .then() and jumps straight to the next .catch().

In this example the first query rejects, so the .then() is skipped entirely and control lands in .catch():

javascript— editable

For a deeper look at rejection patterns, see Error Handling with Promises.

Mid-chain .catch() for recovery

A .catch() does not have to be the last link. Placed in the middle of a chain, it can handle an error, return a fallback value, and let the chain continue. This is the difference between recovering from a failure and aborting the whole sequence.

Below, the first step fails, but a mid-chain .catch() supplies a default and the chain keeps going:

javascript— editable

A mid-chain .catch() recovers and resumes; a terminal .catch() is the final safety net for anything that wasn't recovered earlier.

Cleanup with .finally()

.finally() runs once the promise settles — whether it resolved or rejected. It receives no argument and does not change the value passing through the chain, which makes it ideal for cleanup that must happen either way: hiding a spinner, closing a connection, or re-enabling a button.

javascript— editable

Running Promises in Parallel: Promise.all

Promise.all is not chaining — chaining is sequential (one after another), while Promise.all runs promises in parallel and waits for all of them. Reach for it when the operations don't depend on each other, so there's no reason to wait for one before starting the next.

It takes an iterable of promises and returns a single promise that resolves to an array of their results, in the same order as the input. Any non-promise value in the array (like 42 below) is automatically wrapped in a resolved promise. If any input rejects, the whole Promise.all rejects immediately with that error.

javascript— editable

For Promise.all, Promise.race, Promise.allSettled, and the other combinators, see the Promise API.

Summary

  • Each .then() returns a new promise; the chain reads top to bottom instead of nesting.
  • Return values to pass them on, and return a promise from a handler to make the chain wait for it.
  • The most common bug is a missing return inside .then() — it detaches the inner async work and the next step gets undefined.
  • One terminal .catch() handles errors from any earlier step; a mid-chain .catch() can recover and resume.
  • Use .finally() for cleanup that must run whether the chain succeeded or failed.
  • Use Promise.all for independent work that should run in parallel — that's a different tool from sequential chaining.
  • When you're comfortable here, async/await gives you the same behavior with synchronous-looking syntax.

Practice

Practice
What is the purpose of Promise Chaining in JavaScript?
What is the purpose of Promise Chaining in JavaScript?
Was this page helpful?