Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A JavaScript promise represents the eventual success or failure of asynchronous work. It starts pending, settles once as fulfilled or rejected, and lets you connect later operations with .then() and .catch(). The key to reliable chains is to return each asynchronous operation from its handler: that keeps the work connected, carries its value forward, and lets errors reach the right handler.

What a promise is—and what its states mean

A promise is an object that represents a result that may not be available yet. It has three states:

  • Pending: the work has not settled.
  • Fulfilled: the work succeeded, with a fulfillment value.
  • Rejected: the work failed, with a rejection reason, commonly an error.

Fulfilled and rejected promises are both settled. Settlement is final: a promise cannot later switch from fulfilled to rejected or vice versa. “Resolved” is related but not always identical to “fulfilled.” When a promise is resolved with another promise or thenable, it adopts that object’s eventual state rather than fulfilling immediately with the object itself. MDN’s Promise reference describes this adoption behavior, including for Promise.resolve(value).

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Promise constructor accepts an executor function that receives resolve and reject. Use it when adapting work that must explicitly signal success or failure, such as a callback-based API. Most application code can start from an API that already returns a promise; hand-building a promise around promise-based work is usually unnecessary.

How promises flatten callback hell

Callback hell, sometimes called the “pyramid of doom,” is deeply nested code in which each asynchronous callback starts the next operation. Promises let you attach each next step to the promise returned by the prior operation, so dependent steps can be written at one indentation level. This changes the structure of the code, not the asynchronous nature of the work. MDN’s guide to using promises explains this flattening; a peer-reviewed analysis also discusses how promise reactions express sequential control flow without nested callbacks: Madsen, Lhoták, and Tip, “A Model for Reasoning About JavaScript Promises” (OOPSLA 2017).

These examples are schematic: the function names stand in for application-specific APIs.

// Nested callback shape
getUser(id, (user) => {
  getOrders(user, (orders) => {
    getTotal(orders, (total) => showTotal(total));
  });
});

// Promise chain shape
getUser(id)
  .then((user) => getOrders(user))
  .then((orders) => getTotal(orders))
  .then((total) => showTotal(total))
  .catch(showError);

The chain makes the sequence and shared error path easier to follow. It does not make the operations synchronous or run CPU-heavy work in parallel by itself.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How chaining moves values and creates promises

.then(onFulfilled, onRejected) attaches handlers and returns a new promise. That dependent promise represents what happens when the selected handler runs. A handler’s return value determines the next link:

  • Return an ordinary value, and the next promise fulfills with that value.
  • Return a promise or thenable, and the next promise adopts its eventual state.
  • If the relevant handler is absent, the original fulfillment or rejection passes through.
  • If a handler throws, the promise returned by .then() rejects with that error.

For example, each step below receives the value returned by the prior step:

fetchUser(id)
  .then((user) => fetchOrders(user.id))
  .then((orders) => orders.length)
  .then((count) => console.log(count));

The first handler returns fetchOrders(...). The next handler waits for that promise to settle and receives its fulfillment value, the orders. It then returns the count, which becomes the next handler’s input. This return-and-adopt behavior is the core of promise composition.

Why a missing return can produce undefined

If you start asynchronous work inside a handler but do not return it, the chain cannot wait for that work or receive its result. The handler instead returns undefined implicitly, so its dependent promise fulfills with undefined unless the handler returns something else. The started promise is then detached from the chain, often called a floating promise.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
// Detached work: the next handler receives undefined
fetchUser(id)
  .then((user) => {
    fetchOrders(user.id);
  })
  .then((orders) => console.log(orders)); // undefined

// Connected work: the next handler receives the orders
fetchUser(id)
  .then((user) => {
    return fetchOrders(user.id);
  })
  .then((orders) => console.log(orders));

When an arrow function contains only an expression, the expression is returned implicitly: .then(user => fetchOrders(user.id)). With a block body in braces, write return explicitly.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

How rejection propagates—and when catch recovers

A rejected promise skips fulfillment-only .then() handlers and continues down the chain until it reaches a rejection handler. An exception thrown inside a .then() handler also rejects the promise returned by that .then(), so a later .catch() can handle it. .catch(onRejected) is shorthand for .then(undefined, onRejected).

A catch handler does not preserve failure automatically. If it finishes normally, the promise returned by .catch() is fulfilled with the handler’s return value—or with undefined if it returns nothing. That is recovery: later fulfillment handlers run. To log an error but keep the chain rejected, throw it again or return a rejected promise.

loadData()
  .then(transform)
  .catch((error) => {
    report(error);
    throw error; // the returned promise remains rejected
  });

A catch at the end is useful when the same handling applies to failures across a chain. Handle an error closer to the operation when that failure is intentionally recoverable and you have a fallback that lets the main chain continue. The important distinction is the catch handler’s outcome: returning normally recovers; throwing or returning a rejection keeps failure moving downstream.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When promise handlers run

The executor passed to new Promise(...) runs as part of constructing the promise. By contrast, callbacks registered with .then() are scheduled as microtasks; they do not run synchronously in the current JavaScript stack, even if the promise is already fulfilled. MDN explains this scheduling in its promise guide.

Promise.resolve().then(() => console.log("later"));
console.log("now");
// Prints: now
// Then: later

Promises organize asynchronous results and reactions; they are not threads and do not, by themselves, make CPU-intensive JavaScript run in parallel.

A practical checklist for debugging a chain

  • Unexpected undefined? Check whether each handler returns the value or promise that the next handler needs.
  • Later work runs too early? Look for asynchronous work started inside a handler but not returned.
  • A later step is skipped? Trace whether an earlier operation rejected or a handler threw, and find the next rejection handler.
  • The error disappears after logging? Check whether the catch callback returned normally; rethrow or return a rejected promise if callers must still see failure.
  • Code after .then() logs first? That is expected: promise reactions run after the current stack as microtasks.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.