JavaScript Promises

JavaScript often needs to perform operations that do not finish immediately. Examples include fetching data from an API, reading a file, waiting for a timer, or communicating with a database. These operations are asynchronous because JavaScript can continue executing other code while waiting for the operation to finish.

A Promise is an object that represents the eventual result of an asynchronous operation. It provides a cleaner way to work with asynchronous code compared with deeply nested callbacks. Promises can be resolved when an operation succeeds or rejected when an operation fails.

Promise States

A Promise has three possible states: pending, fulfilled, and rejected. A pending Promise is still waiting for its operation to complete. A fulfilled Promise means the operation completed successfully and produced a result. A rejected Promise means the operation failed and contains an error or reason.

Once a Promise becomes fulfilled or rejected, it is considered settled. A settled Promise cannot change to another state.

JavaScript
A simple Promise that resolves successfully.
const promise = new Promise((resolve, reject) => {
  resolve("Operation completed");
});

console.log(promise);

Creating a Promise

You can create a Promise using the Promise constructor. It receives a function with two parameters: resolve and reject. Call resolve when the asynchronous operation succeeds and reject when it fails.

JavaScript
Creating a Promise with a delayed result.
const fetchData = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Data received");
  }, 1000);
});

console.log(fetchData);

In real applications, you usually consume Promises rather than manually creating them. APIs such as fetch already return Promises.

Handling Successful Results with then()

The then() method runs when a Promise is fulfilled. The resolved value is passed to the callback function.

JavaScript
Reading a successful Promise result.
const promise = Promise.resolve("Hello JavaScript");

promise.then((result) => {
  console.log(result);
});

The callback passed to then() receives the value supplied to resolve(). This makes it possible to continue processing asynchronous results without blocking the rest of the program.

Handling Rejected Promises

The catch() method handles a rejected Promise. It is useful for displaying an error message, retrying an operation, or performing another appropriate recovery action.

JavaScript
Handling a rejected Promise.
const promise = Promise.reject(new Error("Request failed"));

promise
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error.message);
  });

Handling errors explicitly is important because asynchronous operations can fail for many reasons, including network problems, invalid input, unavailable services, or server errors.

Promise Chaining

One of the most useful features of Promises is chaining. A then() callback can return another value or another Promise. The next then() receives the returned result.

JavaScript
Chaining multiple asynchronous steps.
Promise.resolve(10)
  .then((number) => {
    return number * 2;
  })
  .then((number) => {
    return number + 5;
  })
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error);
  });

Promise chaining is useful when several operations depend on the result of previous operations. Each step can transform the previous result and pass it forward.

Running Multiple Promises with Promise.all()

Promise.all() allows multiple independent Promises to run together. It fulfills when all supplied Promises fulfill and returns their results in the same order as the input.

JavaScript
Waiting for multiple Promises.
const first = Promise.resolve("User");
const second = Promise.resolve("Orders");
const third = Promise.resolve("Products");

Promise.all([first, second, third])
  .then((results) => {
    console.log(results);
  })
  .catch((error) => {
    console.error(error);
  });

Promise.all() is useful when several requests are independent and you need all of their results before continuing. If one Promise rejects, the combined Promise rejects.

Using Promise.race()

Promise.race() waits for the first supplied Promise to settle. The first fulfilled or rejected Promise determines the result.

JavaScript
Using the first completed Promise.
const fast = new Promise((resolve) => {
  setTimeout(() => resolve("Fast result"), 500);
});

const slow = new Promise((resolve) => {
  setTimeout(() => resolve("Slow result"), 1500);
});

Promise.race([fast, slow])
  .then((result) => {
    console.log(result);
  });

Promise.race() can be useful when you want whichever operation finishes first, such as implementing a timeout around another asynchronous operation.

Using Promise.allSettled()

Promise.allSettled() is useful when you need information about every Promise regardless of whether it succeeds or fails. Unlike Promise.all(), one rejection does not immediately reject the combined result.

JavaScript
Checking the outcome of every Promise.
const promises = [
  Promise.resolve("Success"),
  Promise.reject("Failed"),
  Promise.resolve("Completed")
];

Promise.allSettled(promises)
  .then((results) => {
    console.log(results);
  });

The returned array contains the status and corresponding value or reason for each Promise. This is helpful for batch operations where individual failures should not prevent other results from being processed.

Promises with the Fetch API

The Fetch API returns a Promise, making it a common real-world example of Promise usage. The first Promise resolves when a response is received, and response.json() returns another Promise for reading JSON data.

JavaScript
Fetching JSON data with Promise chaining.
fetch("https://api.example.com/users")
  .then((response) => {
    if (!response.ok) {
      throw new Error("Request failed");
    }

    return response.json();
  })
  .then((users) => {
    console.log(users);
  })
  .catch((error) => {
    console.error(error.message);
  });

Checking response.ok is useful because an HTTP error response does not automatically reject the Fetch Promise. You can explicitly throw an error when the response is not successful.

Using finally()

The finally() method runs after a Promise settles, whether it was fulfilled or rejected. It is useful for cleanup tasks such as hiding a loading indicator or resetting temporary state.

JavaScript
Running cleanup after a Promise finishes.
fetch("https://api.example.com/data")
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error(error);
  })
  .finally(() => {
    console.log("Request finished");
  });

Common Promise Mistakes

A common mistake is forgetting to return a Promise inside a then() callback when the next step depends on it. Without returning the Promise, the next step may execute before the asynchronous operation has completed.

JavaScript
Returning a Promise correctly in a chain.
getUser()
  .then((user) => {
    return getOrders(user.id);
  })
  .then((orders) => {
    console.log(orders);
  })
  .catch((error) => {
    console.error(error);
  });

Another common mistake is creating unnecessary nested Promises. Promise chaining should normally keep dependent operations in a readable sequence rather than nesting callbacks inside callbacks.

Summary

Promises provide a structured way to handle asynchronous JavaScript operations. A Promise can be pending, fulfilled, or rejected. The then() method handles successful results, catch() handles failures, and finally() performs cleanup after completion.

Promise chaining is useful for sequential asynchronous operations, while Promise.all() is useful when multiple independent operations must all succeed. Promise.race() works with the first settled operation, and Promise.allSettled() lets you inspect the outcome of every operation.

Understanding Promises is an important step toward modern asynchronous JavaScript because async and await are built directly on top of the Promise model.