Promise

Picture yourself as a wildly popular singer, chased day and night by fans who want to know when your next song drops.

Rather than fend them off one by one, you promise to send the track over the moment it’s published. You hand around a list. Fans write down their email addresses, and when the song is ready every subscriber gets it at once. If disaster strikes — a fire in the studio, the recording lost — they still hear about it, because the promise covers failure too.

Everyone comes out ahead. You get some peace, and the fans never miss the release.

That story maps almost perfectly onto a common shape in code:

  1. A producing code that does something slow. Loading data over a network, say. This is the “singer”.
  2. A consuming code that wants the producing code’s result once it exists. Often several pieces of code want it. These are the “fans”.
  3. A promise is a special JavaScript object that ties the producing code to the consuming code. It’s the “subscription list”. The producing code takes as long as it needs, and the promise hands the result to every subscribed consumer once it’s ready.

The analogy is rough — real promises carry more features and stricter rules than a mailing list — but it’s a fine place to start.

producing code
the “singer”
promise
the “list”
consumer
consumer
consumer
A promise sits between the slow producing code and the many consumers waiting on its result.

Building a promise

Here’s the constructor:

let promise = new Promise(function(resolve, reject) {
  // executor (the producing code, "singer")
});

The function you pass to new Promise is the executor. It runs automatically and immediately when the promise is constructed — you don’t call it yourself. Its job is to do the work that eventually yields a result. In our story, the executor is the singer recording the song.

The two arguments, resolve and reject, are callbacks that JavaScript itself supplies. You never write them; you only call them. Everything you write lives inside the executor’s body.

When the work finishes — whether that takes a millisecond or a minute — the executor calls exactly one of them:

  • resolve(value) — the job succeeded, and value is the result.
  • reject(error) — the job failed, and error describes what went wrong.

So the flow is: the executor starts on its own, attempts the work, then signals resolve on success or reject on failure.

The object handed back by new Promise carries two internal properties:

  • state — starts as "pending". It flips to "fulfilled" when you call resolve, or "rejected" when you call reject.
  • result — starts as undefined. It becomes value after resolve(value), or error after reject(error).

So the executor drives the promise from its starting state into one of two final states:

state: “pending”
result: undefined
resolve(value) ↓
state: “fulfilled”
result: value
reject(error) ↓
state: “rejected”
result: error
A new promise begins pending. Calling resolve or reject moves it to a final state and fills in its result.

Soon we’ll see how the “fans” subscribe to these state changes.

Here’s a concrete executor whose producing code takes real time, faked with setTimeout:

let promise = new Promise(function(resolve, reject) {
  // this body runs automatically the moment the promise is constructed

  // after 1 second, signal success with the result "released"
  setTimeout(() => resolve("released"), 1000);
});

Run it and two things stand out:

  1. The executor fires right away, called by new Promise.

  2. It receives resolve and reject ready-made from the engine, so there’s nothing to define. You just call one of them when the work is over.

    After a second of “processing”, the executor calls resolve("released"), which pushes the promise into its fulfilled state:

state: “pending”
result: undefined
resolve(“released”)
state: “fulfilled”
result: “released”
After one second, resolve('released') fulfills the promise and stores 'released' as the result.

That was a clean success — a “fulfilled promise”.

Try it yourself. The promise starts pending; a second later the executor calls resolve, and the .then handler you subscribed reveals the result:

interactiveWatch a promise settle

Now the failing version, where the executor rejects with an error:

let promise = new Promise(function(resolve, reject) {
  // after 1 second, signal failure with an error
  setTimeout(() => reject(new Error("Studio fire")), 1000);
});

The reject(...) call drives the promise into its rejected state:

state: “pending”
result: undefined
reject(error)
state: “rejected”
result: Error: Studio fire
reject(new Error('Studio fire')) rejects the promise and stores the error as the result.

Putting it together: the executor does its (usually slow) job, then calls resolve or reject to settle the matching promise object.

A promise that has been resolved or rejected is called settled. One that’s still waiting is pending.

Consumers: then, catch

A promise bridges the executor (the producing code, the “singer”) and the consuming functions (the “fans”) that want the result or the error. Consumers register themselves through .then and .catch.

then

.then is the fundamental one. Its full form takes two functions:

promise.then(
  function(result) { /* handle a successful result */ },
  function(error) { /* handle an error */ }
);

The first function runs if the promise fulfills, and receives the result. The second runs if the promise rejects, and receives the error. At most one of them fires, decided by how the promise settled.

promise settles
fulfilled ↓
1st fn(result)
rejected ↓
2nd fn(error)
then registers two handlers; the promise's final state selects which one runs.

A reaction to a promise that fulfills:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("released"), 1000);
});

// resolve triggers the first function in .then
promise.then(
  result => alert(result), // shows "released" after 1 second
  error => alert(error) // never runs here
);

The first function fired.

And the mirror case — a rejection triggers the second:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("Studio fire")), 1000);
});

// reject triggers the second function in .then
promise.then(
  result => alert(result), // never runs here
  error => alert(error) // shows "Error: Studio fire" after 1 second
);

Flip the outcome below and see which of the two handlers .then selects. Only ever one fires, decided by how the promise settled:

interactivethen picks one handler

If you only care about success, pass just one function:

let promise = new Promise(resolve => {
  setTimeout(() => resolve("released"), 1000);
});

promise.then(alert); // shows "released" after 1 second

catch

If you only care about errors, you could pass null as the first argument: .then(null, errorHandlingFunction). Cleaner is .catch(errorHandlingFunction), which does exactly the same thing:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Studio fire")), 1000);
});

// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Studio fire" after 1 second

.catch(f) is a straight shorthand for .then(null, f) — nothing more.

Cleanup: finally

Just as a regular try {...} catch {...} can have a finally clause, promises have .finally.

.finally(f) resembles .then(f, f) in one respect: f runs whenever the promise settles, success or failure alike.

The point of finally is a cleanup handler — work you want to run after everything’s over, regardless of outcome. Stopping a loading spinner, closing a connection you no longer need, and so on.

Think of it as tidying up after a party. Good night or bad, big crowd or small, someone still has to clear the dishes.

Roughly:

new Promise((resolve, reject) => {
  /* do something slow, then resolve or maybe reject */
})
  // runs once the promise settles, successfully or not
  .finally(() => stop loading indicator)
  // so the loading indicator is always stopped before we go on
  .then(result => show result, err => show error)

But finally(f) is not simply an alias for then(f, f). Three differences matter:

  1. A finally handler takes no arguments. Inside finally you don’t learn whether the promise succeeded or failed, and that’s by design — cleanup is meant to be outcome-agnostic. In the example above, notice the finally handler has no parameter; the actual result gets handled by the next handler in the chain.

  2. A finally handler passes the result or error straight through to the next handler. Here the value flows through finally into then:

    new Promise((resolve, reject) => {
      setTimeout(() => resolve("value"), 2000);
    })
      .finally(() => alert("Promise ready")) // fires first
      .then(result => alert(result)); // <-- .then shows "value"

    The "value" produced by the first promise sails through finally and lands in then. That’s handy, because finally isn’t there to process results — it’s there for generic cleanup, whatever the outcome.

    And an error passing through finally into catch:

    new Promise((resolve, reject) => {
      throw new Error("error");
    })
      .finally(() => alert("Promise ready")) // fires first
      .catch(err => alert(err)); // <-- .catch shows the error

    (Throwing inside an executor is equivalent to calling reject with that error, which is why this rejects.)

  3. A finally handler shouldn’t return anything. If it does return a value, that value is quietly ignored — the original result or error still passes through. The one exception: if a finally handler throws, that error takes over and flows to the next handler in place of whatever was passing through.

So, gathered up:

  • A finally handler gets no outcome (no arguments); the outcome passes through to the next suitable handler instead.
  • A value returned from finally is ignored.
  • An error thrown from finally diverts execution to the nearest error handler.

Used the way it’s intended — plain cleanup after any outcome — these rules make it behave exactly right.

Run the chain below to watch the order for yourself: finally fires first (with no arguments), then the value or error it passed through lands in the next handler:

interactivefinally runs first, then passes through

Example: loadImage

Time for something closer to real code, to see what promises buy us.

Consider loadImage, which loads a picture and only tells you it’s ready once the browser has fully decoded it. Here’s a callback-based version to start from:

function loadImage(src, callback) {
  let img = document.createElement('img');
  img.src = src;

  img.onload = () => callback(null, img);
  img.onerror = () => callback(new Error(`Image load error for ${src}`));

  document.body.append(img);
}

Let’s rebuild it with a promise. This version takes no callback. Instead it constructs and returns a promise that resolves once loading finishes. Callers subscribe with .then:

function loadImage(src) {
  return new Promise(function(resolve, reject) {
    let img = document.createElement('img');
    img.src = src;

    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Image load error for ${src}`));

    document.body.append(img);
  });
}

Using it:

let promise = loadImage("/media/album-cover.png");

promise.then(
  img => alert(`${img.src} is loaded!`),
  error => alert(`Error: ${error.message}`)
);

promise.then(img => alert('Another handler...'));
img.onload
resolve(img)
img.onerror
reject(error)
promise
.then #1
.then #2
The DOM events onload and onerror become resolve and reject; every .then is a subscriber on the same promise.

Compared with the callback style, a few wins show up right away:

Promises Callbacks
Promises let you work in the natural order. First run loadImage(src), then write with .then what to do with the result. You must already have the callback in hand when you call loadImage(src, callback). You have to know what to do with the result before the call.
You can call .then on a promise as many times as you like. Each call adds a new “fan”, a new subscriber, to the list. More on this in the next chapter: Promise chaining. There’s room for only one callback.

So promises hand you a straighter flow and more flexibility. And there’s more to come in the following chapters.