Promises

Understand what a Promise is, its three states, and how .then, .catch, .finally, and promise chaining let you compose asynchronous operations cleanly.

A Promise is an object representing the eventual result (or failure) of an asynchronous operation. Promises were introduced directly to solve the callback hell problem: instead of nesting callbacks inside callbacks, you chain readable, flat method calls, with built-in, consistent error handling.

What a Promise is

A Promise is an object that wraps an asynchronous operation and represents a value that may not be available yet. You can think of it as a placeholder or an 'IOU' for a future value: you get the Promise object immediately when you start the async operation, and you attach handlers to it to react once the real value (or an error) becomes available.

Promises are built into the language (ES2015/ES6) — you don't need a library to use them, and the vast majority of modern browser and Node.js APIs that do asynchronous work (fetch, file system operations, timers via promisified wrappers) return promises.

The three states: pending, fulfilled, rejected

Every Promise starts out pending — the operation hasn't finished yet, and it holds no value. From pending it can transition exactly once to one of two final states: fulfilled, meaning the operation succeeded and the promise now holds a result value; or rejected, meaning the operation failed and the promise now holds a reason (usually an Error object).

Once a promise settles (becomes fulfilled or rejected), it stays in that state forever — a promise can never go from fulfilled to rejected, or vice versa, and it can never be pending again. This immutability is part of what makes promises predictable and safe to pass around.

then, catch, and finally

.then(onFulfilled, onRejected) attaches callbacks to a promise: onFulfilled runs if the promise is fulfilled (receiving the result value), and the optional onRejected runs if it's rejected (receiving the error). In practice, most code uses just.then(onFulfilled) for the success path..catch(onRejected) is shorthand for.then(undefined, onRejected) — it's the idiomatic way to handle errors, and it also catches errors thrown anywhere earlier in a promise chain, not just from the immediately preceding step..finally(onFinally) attaches a callback that runs once the promise settles, regardless of whether it was fulfilled or rejected — useful for cleanup code (like hiding a loading spinner) that must run either way.

Promise chaining

Each call to .then() returns a brand-new promise, which is what makes chaining possible: you can call .then() again on that returned promise to run another step after the first one completes, and so on, producing a flat, readable sequence instead of a nested pyramid.

If the callback passed to .then() returns a plain value, the next .then() in the chain receives that value directly. If it returns another promise, the chain automatically waits for that inner promise to settle before continuing — this is what lets you sequence multiple asynchronous steps, one after another, without nesting.

Creating and consuming a Promise

new Promise(executor) takes an 'executor' function that receives resolve and reject — call resolve(value) on success, reject(error) on failure. The executor runs immediately (synchronously) when the Promise is constructed, but resolve/reject can be called at any later point, even asynchronously. Only the first call to resolve or reject has any effect — a promise settles once and its state can never change afterward.

const myPromise = new Promise((resolve, reject) => {
  // do async work here
  const success = true;
  if (success) {
    resolve("It worked!");
  } else {
    reject(new Error("It failed."));
  }
});

myPromise
  .then(result => console.log(result))
  .catch(error => console.log(error.message))
  .finally(() => console.log("Done, either way."));

Code Examples

This wraps a timer-based delay in a Promise. The executor calls resolve after 1000ms, which transitions the promise from pending to fulfilled, triggering the.then callback with the resolved value. Expected result: Coffee is ready! (printed after about 1 second) resolve("Coffee is ready!") moves the promise from pending to fulfilled and delivers that string to whoever calls.then on it.

This is the flat alternative to the nested callback hell example from the previous lesson. Returning getOrdersForUser(user) from the first.then makes the chain automatically wait for that promise before running the second.then — and a single.catch at the end handles a failure from either step. Expected result: User: Alex Orders: [ 'Order#1', 'Order#2' ] return getOrdersForUser(user); inside a.then callback makes the next.then wait for that returned promise instead of receiving a promise object as its value.

const orderCoffee = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Coffee is ready!");
  }, 1000);
});

orderCoffee.then(message => {
  console.log(message);
});
function getUser(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id: id, name: "Alex" }), 300);
  });
}

function getOrdersForUser(user) {
  return new Promise(resolve => {
    setTimeout(() => resolve(["Order#1", "Order#2"]), 300);
  });
}

getUser(7)
  .then(user => {
    console.log("User:", user.name);
    return getOrdersForUser(user);
  })
  .then(orders => {
    console.log("Orders:", orders);
  })
  .catch(error => {
    console.log("Something went wrong:", error.message);
  });

Promise states and handlers

A promise begins pending and settles exactly once, into either fulfilled or rejected. .then handles success, .catch handles failure, and .finally runs regardless of which path was taken.

Promise
                │
             pending
                │
        ┌───────┴───────┐
        ↓               ↓
    Fulfilled        Rejected
        │               │
      .then()         .catch()
        └───────┬───────┘
                ↓
             .finally()
         (runs either way)

Important Rules

A Promise represents a value that may not exist yet — it starts pending and settles once, into fulfilled or rejected.

Once settled, a promise's state and value never change again..then(onFulfilled) handles success;.catch(onRejected) handles failure;.finally() runs regardless of outcome.

Each .then() returns a new promise, enabling chaining instead of nesting.

Returning a promise from inside a .then callback makes the chain wait for it before continuing to the next .then.

Where this shows up

Nearly every modern browser API that does asynchronous work returns a Promise: fetch() for network requests, the Cache API, the Clipboard API, and many Node.js 'promises' APIs (like fs.promises.readFile). Promises are also the foundation async/await is built on top of, which is why understanding them deeply pays off in the very next lesson.

getUser(1).then(user => { getOrdersForUser(user).then(orders => console.log(orders)); });

getUser(1).then(user => { return getOrdersForUser(user); }).then(orders => console.log(orders));

Nesting a .then inside another .then recreates the exact same pyramid problem promises were meant to fix. Returning the inner promise instead lets it flatten into the outer chain.

"Once I call resolve() inside a promise executor, I can call resolve() or reject() again later to update the value."

A promise can only settle once — the first call to resolve or reject wins, and every call after that is silently ignored.

Treating a promise as something that can be re-resolved misunderstands its core guarantee: a settled promise's outcome is permanent, which is exactly what makes it safe to hand to multiple .then() consumers.

[Multiple Choice] What happens if you call resolve("first") and then later call resolve("second") on the same promise executor? A. The promise's value updates to "second" B. An error is thrown because resolve was called twice C. Only the first call has any effect — the promise settles as "first" and the second call is ignored D. The promise becomes rejected

C. Only the first call has any effect — the promise settles as "first" and the second call is ignored — A promise can only settle once. The first resolve or reject call determines its final state and value permanently; any subsequent calls are simply ignored.

Write a function checkAge(age) that returns a Promise: resolve with "Access granted" if age >= 18, otherwise reject with a new Error("Access denied"). Then call it for age 20, handling both the success and failure cases, and add a.finally that logs "Check complete." function checkAge(age) { return new Promise((resolve, reject) => { if (age >= 18) { resolve("Access granted"); } else { reject(new Error("Access denied")); } }); } checkAge(20).then(result => console.log(result)).catch(error => console.log(error.message)).finally(() => console.log("Check complete."));

Use new Promise((resolve, reject) => { ... }) with an if/else inside, no setTimeout needed for this one.