JavaScript Promises

A Promise represents a value that will be available now, later, or never. It is the foundation of asynchronous JavaScript and the building block for async/await.

The three states

Every Promise is always in one of three states:

  • Pending: the operation has not completed yet.
  • Fulfilled: the operation succeeded and the Promise has a resolved value.
  • Rejected: the operation failed and the Promise has an error reason.

Think of it like ordering food at a restaurant. You place an order (pending). Either it arrives (fulfilled) or the kitchen says they ran out (rejected). Once it is fulfilled or rejected, it stays that way. A Promise cannot change state twice.

Creating a Promise

You create a Promise with new Promise(). The constructor takes a function with two parameters: resolve (call this on success) and reject (call this on failure).

const fetchUser = (id) => {
  return new Promise((resolve, reject) => {
    if (id > 0) {
      resolve({ id, name: 'Anna' });
    } else {
      reject(new Error('Invalid user ID'));
    }
  });
};

In real code, you rarely create Promises by hand. Most of the time you work with Promises returned by built-in APIs like fetch() or browser APIs like getUserMedia().

.then() and .catch()

.then() runs when the Promise fulfills. .catch() runs when it rejects.

fetchUser(1)
  .then((user) => {
    console.log('Got user:', user.name); // Got user: Anna
  })
  .catch((err) => {
    console.error('Error:', err.message);
  });

You can also handle the rejection inside .then() as a second argument, but using a separate .catch() at the end of the chain is cleaner and catches errors from any step in the chain.

.finally()

.finally() always runs, whether the Promise fulfilled or rejected. Use it for cleanup work that needs to happen regardless of the outcome, like hiding a loading spinner.

let isLoading = true;

fetchUser(1)
  .then((user) => {
    console.log('Got user:', user.name);
  })
  .catch((err) => {
    console.error('Error:', err.message);
  })
  .finally(() => {
    isLoading = false;
    console.log('Done loading');
  });

Chaining Promises

Each .then() returns a new Promise. That means you can chain them to perform a sequence of async steps.

fetchUser(1)
  .then((user) => {
    console.log('Step 1 - Got user:', user.name);
    return user.id; // pass a value to the next .then()
  })
  .then((userId) => {
    console.log('Step 2 - User ID is:', userId);
    return `Profile URL: /users/${userId}`;
  })
  .then((url) => {
    console.log('Step 3 -', url);
  })
  .catch((err) => {
    console.error('Something went wrong:', err.message);
  });

One important rule: if you forget to return inside a .then(), the next step in the chain receives undefined.

Promise.all()

Promise.all() takes an array of Promises and returns a new Promise that fulfills when all of them fulfill. If any one rejects, the whole thing rejects immediately.

Use this when you have multiple independent async operations and you want to run them in parallel.

const getUser1 = fetchUser(1);
const getUser2 = fetchUser(2);
const getUser3 = fetchUser(3);

Promise.all([getUser1, getUser2, getUser3])
  .then((users) => {
    // users is an array: [user1, user2, user3]
    users.forEach((user) => console.log(user.name));
  })
  .catch((err) => {
    // runs if any one of the three rejects
    console.error('One of the requests failed:', err.message);
  });

Promise.allSettled()

Promise.allSettled() (ES2020) also runs all Promises in parallel, but it waits for all of them to finish regardless of whether they fulfilled or rejected. You get back an array of result objects.

Promise.allSettled([fetchUser(1), fetchUser(-1), fetchUser(3)])
  .then((results) => {
    results.forEach((result) => {
      if (result.status === 'fulfilled') {
        console.log('Success:', result.value.name);
      } else {
        console.log('Failed:', result.reason.message);
      }
    });
  });

// Success: Anna
// Failed: Invalid user ID
// Success: Anna

Use Promise.allSettled() when you want to know the outcome of every Promise, even if some fail.

Promise.race()

Promise.race() resolves or rejects as soon as the first Promise in the array settles.

const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error('Request timed out')), 3000)
);

Promise.race([fetchUser(1), timeout])
  .then((user) => console.log('Got user:', user.name))
  .catch((err) => console.error(err.message));

This is a common pattern for adding a timeout to any async operation.

Common mistakes

Forgetting to return inside .then()

// Wrong: the next step gets undefined
fetchUser(1)
  .then((user) => {
    user.name; // no return!
  })
  .then((name) => {
    console.log(name); // undefined
  });

// Right
fetchUser(1)
  .then((user) => {
    return user.name;
  })
  .then((name) => {
    console.log(name); // Anna
  });

Not handling rejections

If you do not attach a .catch() and a Promise rejects, you get an unhandled Promise rejection warning. Always add error handling.

Wrapping a Promise in another Promise unnecessarily

// Unnecessary: fetch() already returns a Promise
const getData = () => {
  return new Promise((resolve) => {
    fetch('/api/data').then((res) => resolve(res.json()));
  });
};

// Simpler
const getData = () => fetch('/api/data').then((res) => res.json());

FAQ

What is the difference between Promise.all and Promise.allSettled?

Promise.all() fails fast: if any Promise rejects, the whole thing rejects immediately. Promise.allSettled() waits for every Promise to finish and gives you a full report of successes and failures. Use Promise.all() when all requests must succeed. Use Promise.allSettled() when you want partial results and can handle individual failures.

Should I use Promises or async/await?

async/await is built on top of Promises. It is usually easier to read, especially for sequences of steps. Use async/await for most cases. Use .then() chains when working with event streams or when chaining multiple operations feels more natural. See the async/await page.

  • async/await : a cleaner syntax built on top of Promises
  • Fetch API : the most common place you will use Promises in practice