Tutorials

JavaScript ES6 Promises

Key Takeaways

  • A promise represents one eventual outcome: a fulfillment value or a rejection reason. Its states are pending, fulfilled, and rejected.
  • Every then() returns a new promise. Return values, returned promises, and thrown errors determine what happens next.
  • Promise handlers run asynchronously as microtasks; creating a promise doesn't move JavaScript onto another thread.
  • async/await works with promises rather than replacing them. Combinators such as Promise.all() remain useful for coordinating independent operations.

JavaScript promises represent the eventual success or failure of an operation and let you compose its result with later work. You observe fulfillment with then(), handle rejection with catch(), and return promises to connect dependent steps.

Start with the execution model, then build a promise, chain operations, and choose how to combine multiple results.

What Is Asynchronous Programming?

Imagine a button that loads a list through an HTTP GET request. A synchronous request on the browser's main thread blocks that thread while it waits, preventing normal interaction and rendering. An asynchronous request lets the browser continue handling other work while the request is in progress.

This doesn't mean your promise callback runs on a separate JavaScript thread. The runtime manages the network operation; the event loop coordinates when JavaScript callbacks execute. Promise handlers use the microtask queue: they run after the current synchronous JavaScript finishes, even when the promise is already fulfilled.

console.log("start");

Promise.resolve("promise callback").then((value) => {
  console.log(value);
});

console.log("end");

// Output:
// start
// end
// promise callback

A long calculation inside a promise handler can still block the main thread. Promises coordinate results; they don't automatically make CPU-heavy code nonblocking. For the server-side execution model, see our explanation of the Node.js event loop.

How JavaScript Promises Work

A promise has exactly three possible states. Fulfilled and rejected are collectively called settled; once settled, its state and result cannot change.

  • Pending: no final outcome yet.
  • Fulfilled: completed successfully with a value.
  • Rejected: failed with a reason, preferably an Error object.
pending -- success --> fulfilled(value)
        -- failure --> rejected(reason)

fulfilled or rejected = settled

resolve(otherPendingPromise)
        --> locked to that promise, but still pending
        --> eventually follows its fulfillment or rejection

Resolved is not another word for fulfilled. It means the outcome is settled or locked in to follow another promise or a thenable—an object with a compatible then() method. Resolving to a pending promise leaves the outer promise pending until the adopted outcome is known.

Creating a Promise

new Promise(executor) accepts one executor function. The constructor calls that function immediately and synchronously, passing it the resolve and reject functions.

const myPromise = (amount) => {
  return new Promise((resolve, reject) => {
    if (amount > 0) {
      resolve("success!");
    } else {
      reject(new Error("failure!"));
    }
  });
};

myPromise(1)
  .then((value) => console.log(value))
  .catch((error) => console.error(error.message));
// Logs: success!

Here, myPromise(1) returns a fulfilled promise; myPromise(0) returns a rejected one. The condition runs immediately, not later. Only the attached handlers are deferred. Reject with an Error rather than a string so callers receive a useful error object, including a stack trace in common runtimes.

This deliberately small example teaches settlement, not asynchronous work. Use the constructor when adapting callback-based work, such as a timer. If an API already returns a promise, consume or return that promise instead of wrapping it in another constructor.

Promise Methods: then, catch, and finally

then(): Handle Fulfillment or Rejection

then(onFulfilled, onRejected) accepts optional handlers. The fulfillment handler receives the value; the rejection handler receives the reason. Omitting a handler passes the corresponding value or rejection along to the next promise.

// Fragment: uses myPromise(amount) from the previous example.
const handleSuccess = (value) => {
  console.log(`${value} it worked!`);
};

const handleError = (error) => {
  console.error(`${error.message} oh no, it failed!`);
};

myPromise(1).then(handleSuccess, handleError);
// Logs: success! it worked!

myPromise(0).then(handleSuccess, handleError);
// Logs: failure! oh no, it failed!

catch(): Handle Failures Earlier in the Chain

catch(onRejected) is shorthand for then(undefined, onRejected). A trailing catch handles rejections propagated through the chain, including exceptions thrown by earlier handlers. It doesn't catch arbitrary synchronous errors elsewhere or errors thrown in an unrelated timer callback.

Promise.resolve("success!")
  .then(
    (value) => {
      console.log(value);
      throw new Error("The success handler failed");
    },
    (error) => {
      console.error("Input rejected:", error.message);
    }
  )
  .catch((error) => {
    console.error("Chain failed:", error.message);
  });

// Logs: success!
// Then: Chain failed: The success handler failed

The second argument to then() handles rejection of its input promise, not errors thrown by its sibling fulfillment handler. Those errors reject the new promise returned by then(), which the following catch() observes.

A catch handler can also recover: returning an ordinary value fulfills the next link with that value. Throw again, or return a rejected promise, when the failure should continue downstream.

finally(): Clean Up Regardless of Outcome

finally(callback) runs after fulfillment or rejection and passes no arguments to the callback. Use it to clear a timer or reset loading state, not to inspect the result. It normally preserves the previous outcome, but throwing or returning a rejected promise from cleanup makes the returned promise reject. The network example below demonstrates timer cleanup.

Promise.resolve(): Normalize a Value

With an ordinary value, Promise.resolve(value) returns a fulfilled promise. With a promise or thenable, it follows that input's outcome; it doesn't force fulfillment. Observe the value rather than relying on a runtime-specific console display of the promise object.

Promise.resolve("Success")
  .then((value) => console.log(value))
  .catch((error) => console.error(error));
// Logs: Success

Promise.reject(): Create a Rejection

Promise.reject(reason) returns a rejected promise. Attach a rejection handler; creating a rejection isn't itself an error-handling strategy, and unhandled rejections can be reported by the runtime.

Promise.reject(new Error("Something failed"))
  .catch((error) => console.error(error.message));
// Logs: Something failed

ES6 Promise Chaining

Every then() returns a new promise. What its handler does determines that new promise's outcome:

  • Return an ordinary value: the next promise fulfills with it.
  • Return another promise: the next promise follows its eventual outcome.
  • Throw an error: the next promise rejects with that error.
  • Return nothing: the next promise fulfills with undefined.

This runnable example first returns a number, then returns a second asynchronous operation:

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

delayValue(2, 100)
  .then((amount) => {
    if (amount <= 0) {
      throw new Error("Amount must be positive");
    }
    return amount * 3;
  })
  .then((amount) => {
    return delayValue(amount + 1, 100);
  })
  .then((total) => console.log(total))
  .catch((error) => console.error(error.message));
// Logs: 7

The chain passes 2 to the first handler, transforms it into 6, waits for the returned promise, and finally receives 7. Change the initial amount to 0 to exercise the rejection path: the fulfillment handlers after the throw are skipped, and the catch receives the error.

For more sequencing examples, continue with our dedicated guide to JavaScript promise chaining.

Combining Multiple JavaScript Promises

A combinator turns multiple inputs into one promise. These methods accept an iterable, such as an array, and can accept ordinary values as well as promises. They coordinate results; they don't automatically move work onto parallel JavaScript threads.

MethodFulfillment conditionRejection conditionUse it when
Promise.all()Every input fulfills; returns values in input order.An input rejects; uses the first rejection reason.You need every operation to succeed.
Promise.allSettled()Every input settles; returns outcome records in input order.Individual input rejections become records, not an aggregate rejection.You need a complete success/failure report.
Promise.race()The first input to settle fulfills.The first input to settle rejects.You need the first outcome, success or failure.
Promise.any()The first input fulfills.Every input rejects; returns an AggregateError.Any successful result will do.

For valid input iterables, these settlement rules distinguish the four strategies.

Promise.all() and Promise.race(): Correct Timer Examples

const p1 = new Promise((resolve) => {
  setTimeout(() => resolve("p1 success"), 2000);
});

const p2 = new Promise((resolve) => {
  setTimeout(() => resolve("p2 success"), 4000);
});

Promise.all([p1, p2])
  .then((values) => console.log("all:", values))
  .catch((error) => console.error("all failed:", error));

Promise.race([p1, p2])
  .then((value) => console.log("race:", value))
  .catch((error) => console.error("race failed:", error));

With ordinary scheduling, race logs p1 success after about two seconds, while all logs both values after about four seconds. Timer delays aren't deadlines and may run later. Crucially, setTimeout(() => resolve(...), delay) passes a callback; setTimeout(resolve(...), delay) calls resolve immediately.

Promise.all() preserves input order, not completion order, and rejects as soon as an input rejects. Neither that rejection nor winning a race cancels the remaining operations.

Promise.allSettled() and Promise.any(): Partial Success

const inputs = [
  Promise.reject(new Error("Primary unavailable")),
  Promise.resolve("Backup result")
];

Promise.allSettled(inputs)
  .then((outcomes) => {
    console.log(outcomes[0].status, outcomes[1].status);
  })
  .catch((error) => console.error(error));
// Logs: rejected fulfilled

Promise.any(inputs)
  .then((value) => console.log(value))
  .catch((error) => console.error(error));
// Logs: Backup result

An all-settled record contains status plus either value for fulfillment or reason for rejection. Any ignores individual rejections while a fulfillment remains possible. For empty inputs, all and all-settled fulfill with an empty array, any rejects with an AggregateError, and race stays pending.

How Promises Relate to async/await

An async function always returns a promise. Await pauses that function's execution until the awaited outcome is available; a rejection becomes a thrown error at the await expression. This often reads well for dependent steps, while promise combinators remain useful for coordinating independent work.

// Fragment: uses delayValue(value, milliseconds) defined above.
async function calculateTotal() {
  const amount = await delayValue(2, 100);
  const total = await delayValue(amount * 3 + 1, 100);
  return total;
}

calculateTotal()
  .then((total) => console.log(total))
  .catch((error) => console.error(error));
// Logs: 7

Neither syntax supersedes the other. Choose the clearest expression of the dependencies, and see our JavaScript async/await example for a longer walkthrough.

Fetch Errors, Cleanup, and Cancellation

fetch() already returns a promise. An HTTP error status such as 404 doesn't by itself reject it: inspect response.ok, which is true for statuses 200–299, and throw when your application considers the response unsuccessful. Reading JSON with response.json() also returns a promise.

Promises have no built-in cancellation method. To stop a supported operation, use its cancellation API. For fetch, pass an AbortController's signal and call abort(); aborting an unfinished request with the default reason produces an AbortError rejection.

// Browser fragment: your server must expose GET /api/items as JSON.
async function loadItems(signal) {
  const response = await fetch("/api/items", { signal });
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

loadItems(controller.signal)
  .then((items) => console.log(items))
  .finally(() => clearTimeout(timeoutId))
  .catch((error) => {
    if (error.name === "AbortError") {
      console.log("Request aborted");
    } else {
      console.error("Loading failed:", error);
    }
  });

The timer attempts to abort after five seconds; cleanup clears it when the operation settles. You can also call controller.abort() from a cancel action. This aborts the underlying operation, not a promise object, and a timeout implemented only with Promise.race() wouldn't stop the request.

A promise represents one eventual outcome, not an ongoing sequence of events. For repeated emissions, compare Observables and promises.

Common Mistakes

  • Forgetting to return: an unreturned promise isn't connected to the surrounding chain.
  • Accidentally swallowing errors: a catch handler that only logs and returns nothing changes the downstream outcome to fulfillment with undefined. Recover deliberately or rethrow.
  • Treating finally as an error handler: cleanup normally preserves rejection; the resulting promise still needs a rejection handler.
  • Expecting all to throttle requests: it aggregates inputs, not a queue of functions to execute. Call operations to obtain promises, and add a concurrency limit when a batch would start too much work at once.

FAQ

Why use promises over callbacks?

Promises let you return asynchronous results, compose dependent operations, and share error handling across a chain. They still use callbacks through then and catch, but provide consistent rules for forwarding values and failures.

What are the three states of a JavaScript promise?

The three states are pending, fulfilled, and rejected. Fulfilled and rejected are settled states. Resolved means the outcome is settled or locked in to follow another promise or thenable, so a resolved promise can still be pending.

What's the difference between a promise and async/await?

A promise represents an eventual outcome; async/await is syntax for producing and consuming promises. Async functions return promises, and await lets you express dependent operations in a sequential style. Promise methods remain useful for composition and concurrency.

Does Promise.all cancel the other promises when one rejects?

No. Promise.all rejects when an input rejects, but the remaining operations continue unless you explicitly stop them through an API such as AbortController.

Can promises replace all asynchronous operations?

No. A promise represents one eventual result, so repeated events and ongoing data streams need an additional abstraction or API. Callback-based APIs can also remain appropriate, although you can adapt a one-result callback operation to return a promise.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me