Promises in JavaScript

Start independent asynchronous operations concurrently instead of awaiting each one sequentially. Then choose the combinator based on what counts as success.

Choose a Promise combinator by its success rule.
NeedUseSettles when
Every operation must succeedPromise.allAll fulfill; rejects as soon as one rejects.
Keep every success and failurePromise.allSettledEvery input settles; returns an outcome for each.
First result, success or failurePromise.raceThe first input settles.
First successful resultPromise.anyThe first input fulfills; rejects with AggregateError if all reject.
Parallel work with Promise.any()
const primary = fetch("https://api.example.com/data");
const backup = fetch("https://backup.example.com/data");

Promise.any([primary, backup])
  .then(response => response.json())
  .then(data => console.log("First available response:", data))
  .catch(error => {
    console.error("Every source failed:", error.errors);
  });

Concurrency example: if a dashboard needs users, settings, and notifications but none depends on another, start all three before awaiting them. If the settings request needs the user ID, await the user first, then start the dependent request.

Independent requests start concurrently
async function loadDashboard() {
  const [users, settings, notifications] = await Promise.all([
    getUsers(),
    getSettings(),
    getNotifications()
  ]);

  return { users, settings, notifications };
}
Remember: Promise combinators coordinate Promises; they do not automatically cancel the other operations. Use cancellation APIs such as AbortController when unfinished work is no longer needed.

A Promise in JavaScript represents the eventual result of an asynchronous operation. It can eventually be fulfilled with a value or rejected with a reason. Your code can attach handlers for that eventual result without treating the Promise itself as the operation that performs the work.

Before Promises, developers relied heavily on callbacks. When many asynchronous steps were nested, the control flow could become harder to read and maintain, often called callback hell.

Callback Hell (hard to read)
getUser(1, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      // deeply nested and hard to follow
    });
  });
});

Promises improve this by giving you a clean chain. Instead of nesting functions inside functions, you move step by step with .then().

The same logic using Promises (cleaner)
getUser(1)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(details => console.log(details))
  .catch(error => console.error(error));

A Promise is created using the built-in Promise constructor and always produces one of three possible states.

Every Promise exists in exactly one of three states at any given moment:

  • Pending - The initial state. The operation has not finished yet.
  • Fulfilled - The operation completed successfully and the Promise has a result value.
  • Rejected - The operation failed and the Promise has a reason (an error).

Once a Promise moves from pending to either fulfilled or rejected, it is settled and its state can never change again.

Promise state lifecycle
// A Promise starts as "pending"
const myPromise = new Promise((resolve, reject) => {
  // The executor runs immediately; async work can start here
});

// After calling resolve() it becomes "fulfilled"
// After calling reject()  it becomes "rejected"
// Once settled, the state is locked forever

Observing state changes with .then() and .catch()
const p = new Promise((resolve, reject) => {
  resolve("Success!");       // moves to fulfilled
  reject("This is ignored"); // too late, already settled
});

p.then(value => console.log(value));   // "Success!"

You create a Promise using the new Promise() constructor. It takes a single function called the executor function. The executor runs immediately when the constructor is called and receives two arguments: resolve and reject. Calling resolve(value) fulfills the Promise, while calling reject(reason) rejects it. Only the first settlement takes effect; later calls are ignored.

Basic Promise constructor syntax
const myPromise = new Promise(function(resolve, reject) {
  // Do async work here
  const success = true;

  if (success) {
    resolve("The operation worked!");
  } else {
    reject(new Error("Something went wrong."));
  }
});

In real-world code, you often wrap async operations like setTimeout, network requests, or file reads inside a Promise.

Simulating a delayed response with setTimeout
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id: 1, status: "shipped" }); // simulate a server response
    }, 1000);
  });
}

fetchData().then(data => console.log(data));
// After 1 second: { id: 1, status: "shipped" }

You can also use the shorthand static methods Promise.resolve() and Promise.reject() to create already-settled Promises instantly. These are useful in tests or when you want to return a value that already looks like a Promise.

Promise.resolve() and Promise.reject() shortcuts
const resolved = Promise.resolve(42);
resolved.then(v => console.log(v)); // 42

const rejected = Promise.reject(new Error("Oops!"));
rejected.catch(e => console.error(e.message)); // Oops!

Once you have a Promise, you react to its result using:

  • .then(onFulfilled, onRejected) - handles fulfillment and can also receive a rejection handler. The fulfillment handler receives the fulfilled value.
  • .catch(onRejected) - runs when the Promise is rejected. Receives the rejection reason (usually an error).
  • .finally(onFinally) - runs after the Promise settles regardless of success or failure. Useful for cleanup.
Using .then() and .catch()
function divide(a, b) {
  return new Promise((resolve, reject) => {
    if (b === 0) {
      reject(new Error("Cannot divide by zero"));
    } else {
      resolve(a / b);
    }
  });
}

divide(10, 2)
  .then(result => console.log("Result:", result)) // Result: 5
  .catch(error => console.error("Error:", error.message));

divide(10, 0)
  .then(result => console.log("Result:", result))  // skipped
  .catch(error => console.error("Error:", error.message)); // Error: Cannot divide by zero
Using .finally() for cleanup
function loadData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("Data loaded"), 500);
  });
}

loadData()
  .then(data => console.log(data))
  .catch(err => console.error(err))
  .finally(() => console.log("Loading complete")); // always runs

An important detail: .then() itself returns a new Promise. This is what makes chaining possible.

Because .then() always returns a new Promise, you can chain multiple .then() calls to run asynchronous steps one after another. Returning a normal value from a callback fulfills the next Promise with that value. Returning a Promise makes the chain wait for it, while throwing an error causes the next Promise in the chain to reject.

Chaining .then() calls
function getOrder(id) {
  return Promise.resolve({ id: id, status: "shipped" });
}

function getTracking(order) {
  return Promise.resolve({ orderId: order.id, location: "Distribution Center" });
}

function displayResult(data) {
  console.log(`Order ${data.orderId} is at ${data.location}`);
}

getOrder(1)
  .then(order => getTracking(order))    // returns a new Promise
  .then(data => displayResult(data))    // runs after getTracking settles
  .catch(error => console.error(error));

// Order 1 is at Distribution Center

You only need one .catch() at the end of the chain. If any step throws an error or returns a rejected Promise, the chain skips all remaining .then() handlers and jumps straight to .catch().

Error skips to .catch() no matter where it occurs
Promise.resolve(10)
  .then(n => n * 2)                         // 20
  .then(n => { throw new Error("Oops!"); }) // triggers error
  .then(n => n + 1)                         // SKIPPED
  .catch(err => console.error(err.message)); // "Oops!"

Keep your chains flat. Avoid returning a .then() inside another .then() callback unnecessarily as that brings back the nesting problem Promises were designed to solve. For longer or more branching workflows, async/await can make the control flow easier to read; choose the style that keeps the dependencies and error handling clear.

Practical rule: Use Promise.all() when independent operations can be started concurrently, and use async/await when it makes a longer or branching workflow easier to read. The important part is to preserve the dependency order and handle failures deliberately.

Promise.all() takes an iterable of Promises or values and returns a new Promise. That new Promise fulfills when all of the inputs fulfill, with results in input order. It rejects if any one of them rejects, using that rejection reason.

Promise.all() - all must succeed
const p1 = Promise.resolve("User data");
const p2 = Promise.resolve("Order data");
const p3 = Promise.resolve("Product data");

Promise.all([p1, p2, p3])
  .then(results => {
    console.log(results);
    // ["User data", "Order data", "Product data"]
  })
  .catch(error => console.error("One failed:", error));

The resolved value is an array of results in the same order as the input array, regardless of which Promise resolved first. This makes it easy to match results to their source.

If one Promise rejects, all is rejected
const fast = Promise.resolve("Fast");
const failing = Promise.reject(new Error("Network error"));
const slow = Promise.resolve("Slow");

Promise.all([fast, failing, slow])
  .then(results => console.log(results))       // SKIPPED
  .catch(error => console.error(error.message)); // "Network error"

Use Promise.all() when all operations must succeed and you want to start independent asynchronous operations concurrently rather than waiting for each one to complete before starting the next.

If independent asynchronous operations are awaited one after another, each operation may delay the start of the next one. Starting independent operations first and using Promise.all() allows them to run concurrently.

Promise.allSettled() takes an iterable of Promises or values. Unlike Promise.all(), it always fulfills after every input settles, whether fulfilled or rejected, with an array of outcome objects describing each input.

Each result object has:

  • { status: "fulfilled", value: ... } for successful Promises
  • { status: "rejected", reason: ... } for failed Promises
Promise.allSettled() - reports every outcome
const p1 = Promise.resolve("Success A");
const p2 = Promise.reject(new Error("Failed B"));
const p3 = Promise.resolve("Success C");

Promise.allSettled([p1, p2, p3]).then(results => {
  results.forEach(result => {
    if (result.status === "fulfilled") {
      console.log("OK:", result.value);
    } else {
      console.log("FAIL:", result.reason.message);
    }
  });
});

// OK: Success A
// FAIL: Failed B
// OK: Success C

Use Promise.allSettled() when you want to start multiple independent operations concurrently and handle every result individually, even if some fail.

Promise.race() takes an iterable of Promises or values and returns a new Promise that settles as soon as the first input settles, whether fulfilled or rejected.

Promise.race() - first to settle wins
const slow = new Promise(resolve => setTimeout(() => resolve("Slow"), 2000));
const fast = new Promise(resolve => setTimeout(() => resolve("Fast"), 500));
const medium = new Promise(resolve => setTimeout(() => resolve("Medium"), 1000));

Promise.race([slow, fast, medium])
  .then(winner => console.log(winner)); // "Fast" (resolves first)

A practical use case is implementing timeouts. You race your actual operation against a timeout Promise. If the timeout settles first, you treat the operation as failed.

Using Promise.race() to implement a timeout
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

const fetchSomething = new Promise(resolve =>
  setTimeout(() => resolve("Data"), 3000)
);

withTimeout(fetchSomething, 1000)
  .then(data => console.log(data))
  .catch(err => console.error(err.message)); // "Timed out after 1000ms"

Note: Promise.race() does not cancel the losing Promise. Other operations that have already started can continue to completion and their results are ignored once the winner settles. If the original operation should be cancelled after a timeout, use an API that supports cancellation, such as AbortController with fetch().

Promise.any() - Promise.any() fulfills as soon as the first input fulfills, ignoring rejections while it waits. It rejects with an AggregateError only if every input in the iterable rejects.

Here is a quick reference for everything covered in this tutorial:

Concept What it does
new Promise(executor) Creates a new Promise; executor receives resolve and reject
Pending Initial state; the async work is still in progress
Fulfilled Operation succeeded; resolve(value) was called
Rejected Operation failed; reject(reason) was called
.then() Handles fulfillment and can handle rejection; returns a new Promise (enables chaining)
.catch() Handles a rejected Promise; catches errors from the whole chain
.finally() Runs after the Promise settles, regardless of outcome
Promise.resolve(v) Returns an already-fulfilled Promise with value v
Promise.reject(r) Returns an already-rejected Promise with reason r
Promise.all(arr) Fulfills when all input Promises fulfill; rejects if any rejects
Promise.allSettled(arr) Always fulfills; returns an array of outcome objects for every Promise
Promise.race(arr) Settles as soon as the first input Promise settles
Promise.any(arr) Fulfills with the first successful result; rejects only if all reject

Videos for this topic will be added soon.

Reviewed by

SimplyJavaScript Editorial Team

Technical editors and JavaScript educators with hands-on experience building frontend projects, writing learning material, and reviewing tutorials for clarity, accuracy, and beginner-friendly guidance.