Promises

A Promise is an object that represents the eventual completion or failure of an async operation. A callback does not give you anything back, but a Promise does. It is a value, so you can chain it, compose it, and pass it around.

Promise States

A Promise is always in one of three states:

  • Pending: The operation has not completed yet
  • Fulfilled: The operation completed successfully (with a value)
  • Rejected: The operation failed (rejected with a reason)

Once a Promise is fulfilled or rejected, it is settled and its state cannot change.

Creating Promises

Use the Promise constructor, which takes an executor function with resolve and reject parameters:

const promise = new Promise((resolve, reject) => {
  const success = true;
  if (success) {
    resolve("It worked!");
  } else {
    reject(new Error("Something failed"));
  }
});

Here is a more realistic example that simulates a network request:

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: "Alice" });
      } else {
        reject(new Error("Invalid user ID"));
      }
    }, 1000);
  });
}

Consuming Promises

Use .then() to handle fulfillment, .catch() to handle rejection, and .finally() for cleanup that runs either way:

fetchUser(1)
  .then((user) => {
    console.log(user.name); // "Alice"
  })
  .catch((error) => {
    console.error(error.message);
  })
  .finally(() => {
    console.log("Request complete");
  });

.catch() is just shorthand for .then(null, errorHandler). It catches rejections from any preceding .then() in the chain, not just the original Promise.

Promise Chaining

Each .then() returns a new Promise, and that Promise resolves to whatever the handler returns. That is what lets you run async operations one after another:

function lookupCity(name) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id: 2643743, name: "London" }), 500);
  });
}

function fetchForecast(cityId) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ temp: 18, condition: "Cloudy" }), 500);
  });
}

lookupCity("London")
  .then((city) => {
    console.log("City:", city.name); // "City: London"
    return fetchForecast(city.id);
  })
  .then((forecast) => {
    console.log("Forecast:", forecast); // { temp: 18, condition: 'Cloudy' }
  })
  .catch((error) => {
    console.error("Error:", error.message);
  });

Key rules of chaining:

  • Return a value: The next .then() receives it as its argument
  • Return a Promise: The next .then() waits for it to settle
  • Throw an error: The chain skips to the nearest .catch()
  • A single .catch() at the end handles errors from any step in the chain

Passing Data Between Handlers

Each .then() handler only receives the return value from the previous handler. If you need data from earlier in the chain, pass it forward:

lookupCity("London")
  .then((city) => {
    return fetchForecast(city.id).then((forecast) => ({ city, forecast }));
  })
  .then(({ city, forecast }) => {
    console.log(`${city.name}: ${forecast.temp}°C, ${forecast.condition}`);
  });

You can also use async/await (covered in a later section), which keeps the earlier variables in scope for you.

Creating Resolved/Rejected Promises

There are static methods that give you a Promise that is already settled:

const resolved = Promise.resolve("done");
resolved.then((val) => console.log(val)); // "done"

const rejected = Promise.reject(new Error("fail"));
rejected.catch((err) => console.error(err.message)); // "fail"

These are useful when you need to start a Promise chain, or when a function has to return a Promise and you already have the value.