A Deep Dive into JavaScript Promises: resolve, reject, .then(), .catch(), and .finally()

Published on: by Dr. Talib

While `async/await` provides a clean syntax for handling asynchronous tasks, it is built entirely on top of a more fundamental concept: the **Promise**. Understanding how Promises work under the hood is crucial for any advanced JavaScript developer. A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.


The States of a Promise

A Promise can be in one of three states:

  • Pending: The initial state; the operation has not yet completed.
  • Fulfilled (or Resolved): The operation completed successfully, and the Promise now has a resulting value.
  • Rejected: The operation failed, and the Promise has a reason for the failure (an error).

Once a Promise is either fulfilled or rejected, it is considered "settled" and its state can never change again. This immutability is a key feature that makes them reliable.

Creating a Promise

While you will most often *consume* Promises returned by Web APIs (like `fetch`), it's important to know how to create one. The `Promise` constructor takes a single argument: an "executor" function. This function itself receives two arguments: `resolve` and `reject`.

Example of Creating a Promise:

const myPromise = new Promise((resolve, reject) => {
  // Simulate an asynchronous operation, like a network request
  setTimeout(() => {
    const success = true; // Change to false to see the rejection

    if (success) {
      // If the operation was successful, call resolve() with the result
      resolve("The data has been fetched successfully!");
    } else {
      // If it failed, call reject() with an error
      reject(new Error("Failed to fetch data."));
    }
  }, 2000);
});

Consuming a Promise with `.then()`, `.catch()`, and `.finally()`

Once you have a Promise object, you attach handlers to it to react to its eventual settlement.

  • .then(onFulfilled, onRejected): The primary method. It takes up to two arguments: a function to run if the Promise is fulfilled, and an optional function to run if it's rejected. It returns a *new* Promise, which is why you can chain them.
  • .catch(onRejected): This is just syntactic sugar for .then(null, onRejected). It's the standard way to handle any errors that occur anywhere in the Promise chain.
  • .finally(onFinally): This takes a function that will run whether the Promise is fulfilled or rejected. It's perfect for cleanup tasks, like hiding a loading spinner.

Example of Consuming Our Promise:

// (Assuming myPromise from the previous example exists)

console.log("Promise is pending...");

myPromise
  .then((successMessage) => {
    // This block runs if the Promise resolves
    console.log("Success:", successMessage);
  })
  .catch((errorMessage) => {
    // This block runs if the Promise rejects
    console.error("Error:", errorMessage.message);
  })
  .finally(() => {
    // This block runs no matter what
    console.log("Promise has settled.");
  });

Try it Yourself: Paste both the creation and consumption code into the HTML Viewer. First, run it with `success = true`. Then, change it to `success = false` and run it again to see the `.catch()` block execute instead.

Chaining Promises

The real power of .then() is that it returns a new Promise. This allows you to chain asynchronous operations in a clean sequence. If you return a value from a .then(), it gets passed as the argument to the next .then(). If you return another Promise, the chain will wait for that new Promise to resolve before continuing.

Promise.resolve(10) // Start with a resolved Promise with value 10
  .then(value => {
    console.log("Step 1:", value); // Logs 10
    return value * 2; // Return a new value
  })
  .then(value => {
    console.log("Step 2:", value); // Logs 20
    // Return a new Promise that resolves after 1 second
    return new Promise(resolve => setTimeout(() => resolve(value + 100), 1000));
  })
  .then(value => {
    console.log("Step 3:", value); // Logs 120 after a 1-second delay
  });

The Foundation of Modern Asynchronicity

Promises solved the infamous "Callback Hell" problem and provided a sane, reliable way to manage asynchronous operations. While async/await offers a more readable syntax for *consuming* Promises, understanding the underlying `.then()` and `.catch()` mechanism is essential for debugging, writing more complex asynchronous logic, and truly mastering modern JavaScript.