A Promise is an object that represents a value which isn't available yet, but will be at some point — either successfully (resolved) or with an error (rejected). Promises were added to JavaScript specifically to clean up the deeply nested, hard-to-follow code that plain callbacks led to once you chained several async steps together.

The Three States of a Promise

Every Promise starts in one state and can only move forward, never backward:

  • Pending — the initial state, the result isn't ready yet.
  • Fulfilled — the operation succeeded, and a value is available.
  • Rejected — the operation failed, and an error reason is available.

Creating a Promise

function wait(ms) {
  return new Promise((resolve, reject) => {
    if (ms < 0) {
      reject(new Error("Duration can't be negative"));
      return;
    }
    setTimeout(() => {
      resolve(`Waited ${ms}ms`);
    }, ms);
  });
}

A Promise constructor takes a function with two parameters, resolve and reject — you call resolve(value) on success, or reject(error) on failure. In practice, you'll usually be consuming Promises returned by built-in APIs like fetch() far more often than constructing your own like this.

Consuming a Promise with .then() and .catch()

wait(500)
  .then((result) => {
    console.log("Success:", result);
  })
  .catch((err) => {
    console.log("Failed:", err.message);
  });

console.log("This logs first, before the wait finishes");

Output:

This logs first, before the wait finishes
Success: Waited 500ms

.then() runs only if the Promise resolves, and receives the resolved value. .catch() runs only if it rejects, and receives the error. The rest of your code keeps running immediately after calling wait(500) — it doesn't pause and wait.

Chaining Promises

Returning a value (or another Promise) from inside .then() lets you chain steps in sequence, flat instead of nested — directly solving the callback-hell problem:

wait(100)
  .then((result) => {
    console.log(result);
    return wait(100); // returning a Promise chains the next step
  })
  .then((result) => {
    console.log(result, "(second wait)");
  })
  .catch((err) => console.log("Something failed:", err.message));

This flat chain is already a big readability improvement over nested callbacks, but there's an even cleaner way to write the exact same logic: async/await, which lets asynchronous code read almost like ordinary synchronous code.