jsguides

Promise.prototype.then()

then(onFulfilled)

The then() method attaches callback functions for the fulfillment and rejection cases of a Promise. It returns a new Promise, enabling method chaining for asynchronous operations.

Syntax

promise.then(onFulfilled)
promise.then(onFulfilled, onRejected)

Parameters

ParameterTypeDefaultDescription
onFulfilledfunctionundefinedCalled if the Promise is fulfilled. Receives the fulfillment value as an argument.
onRejectedfunctionundefinedCalled if the Promise is rejected. Receives the rejection reason (error) as an argument.

Return Value

A new Promise that:

  • Resolves with the return value of onFulfilled if called
  • Resolves with the original value if onFulfilled is not a function
  • Rejects with the error thrown by onRejected if it throws
  • Rejects with the original rejection reason if onRejected is not a function

Examples

Basic Usage

const promise = Promise.resolve(42);

promise.then(value => {
  console.log(value); // 42
  return value * 2;
}).then(double => {
  console.log(double); // 84
});

The first then() receives the resolved value 42, doubles it, and returns 84. The second then() picks up that return value directly—no nesting, no callbacks inside callbacks. This flat chain is the main reason then() replaced older callback patterns, and the next example shows it at work with real network requests.

Chaining multiple promises

fetch("/api/user")
  .then(response => response.json()) // Returns a Promise
  .then(user => {
    console.log("User:", user.name);
    return fetch(`/api/posts/${user.id}`);
  })
  .then(response => response.json())
  .then(posts => {
    console.log("Posts:", posts.length);
  })
  .catch(error => {
    console.error("Request failed:", error);
  });

The fetch chain above shows the standard pattern: call the API, parse JSON, use the result to make another request, and handle any failure with catch() at the end. A single catch() at the tail of a chain captures rejections from any earlier step, so you do not need error handlers on every then(). The next example isolates that error-handling behavior on its own.

Handling Errors

fetchData()
  .then(data => processData(data))
  .then(result => console.log("Success:", result))
  .catch(error => {
    console.error("Failed:", error.message);
  });

Placing catch() at the end works well when any failure should stop the whole chain. But sometimes you want to recover from a specific rejection and continue. For that, you pass a second callback—the rejection handler—directly to then(). This lets you handle the error at that step and return a fallback value that feeds into the rest of the chain.

Using both callbacks

const promise = Promise.reject(new Error("Initial error"));

promise.then(
  value => console.log("Fulfilled:", value),
  error => {
    console.log("Caught:", error.message); // "Caught: Initial error"
    return "Recovered value";
  }
).then(value => {
  console.log(value); // "Recovered value"
});

The rejection handler caught the error, logged it, and returned “Recovered value.” The next then() in the chain sees that recovery value, not the error—the chain picks up where it left off. The example below shows the key behavior that makes all of this work: then() always returns a new promise, and the type of value you return from a handler determines how the next step behaves.

Returning values vs. promises

// Returning a non-Promise value
Promise.resolve(1)
  .then(x => x + 1) // 2
  .then(x => x * 2)  // 4
  .then(console.log);

// Returning a Promise (automatically chained)
Promise.resolve(1)
  .then(x => Promise.resolve(x + 1)) // Resolves to 2
  .then(x => Promise.resolve(x * 2))  // Resolves to 4
  .then(console.log);

Whether you return a plain value or a promise from a then() handler, the next handler in the chain receives the resolved value in the same way. Returning a promise simply delays the next step until that inner promise settles. This uniformity is what lets you mix synchronous and asynchronous logic without special syntax. The next example combines then() with async/await to show both styles working together.

Sequential async operations

async function loadFullStory(chapterId) {
  const chapter = await fetch(`/api/chapters/${chapterId}`)
    .then(r => r.json());
  
  const author = await fetch(`/api/authors/${chapter.authorId}`)
    .then(r => r.json());
  
  const reviews = await fetch(`/api/reviews?book=${chapter.bookId}`)
    .then(r => r.json());
  
  return { chapter, author, reviews };
}

Mixing await and then() is valid, though most codebases pick one style and stick with it. The examples below cover the two most common mistakes with then(): forgetting to return a value from a handler, which drops data from the chain, and failing to handle rejections at all.

Common Mistakes

Forgetting to Return

// WRONG: Missing return statement
fetch("/api/user")
  .then(response => response.json())
  .then(user => {
    console.log(user.name); // Works
  })
  .then(() => {
    console.log("User processed"); // user is undefined here!
  });

// CORRECT: Return the chain
fetch("/api/user")
  .then(response => response.json())
  .then(user => {
    console.log(user.name);
    return user;
  })
  .then(user => {
    console.log("User processed:", user.name);
  });

When a then() handler does not return a value, the next handler receives undefined. This is easy to miss because the chain still runs—it just runs with empty data. The second common pitfall is skipping error handling entirely, which produces unhandled promise rejections that can crash Node.js processes or go silently uncaught in browsers.

Not handling rejections

// WARNING: Unhandled rejection
fetch("/api/missing")
  .then(response => response.json());

// FIX: Always handle rejections
fetch("/api/missing")
  .then(response => response.json())
  .catch(error => {
    console.error("Request failed:", error.message);
  });

How then() enables chaining

Each call to then() returns a new Promise. The returned promise resolves with whatever value the onFulfilled handler returns. If the handler returns a plain value, the new promise resolves immediately with that value. If it returns a Promise (or any thenable), the new promise “follows” that thenable — it waits for the inner promise to settle and adopts its state. This is what allows a chain of then() calls to sequence asynchronous operations without nesting.

then() vs async/await

then() and async/await compile to the same underlying promise machinery. async/await syntax is generally preferred in new code because it reads synchronously, handles errors with standard try/catch, and is easier to debug with stack traces. Use then() when composing existing promises in utility functions, or when you need access to the intermediate promise reference for cancellation or racing.

Returning values from then()

The value returned from the onFulfilled callback becomes the resolution value of the promise returned by then(). If the callback returns a plain value, the next then() in the chain receives it immediately. If it returns a promise or a thenable, the chain waits for that promise to settle before continuing. If the callback throws, the returned promise rejects with the thrown error, and the next rejection handler in the chain receives it.

See Also