---
title: 'The Promise Concept'
source: 'https://academia.sh/en/courses/asynchronous-javascript/promise-concept'
course: 'Asynchronous JavaScript and the Runtime'
language: en
updated: '2026-08-17T18:09:44+00:00'
license: 'CC BY-SA 4.0'
---

# The Promise Concept

A promise's three-state machine, the result being decided exactly once, callbacks being deferred to a microtask, and each step of chaining producing a new promise.

The previous lesson closed with a gap: in the callback pattern, an asynchronous
operation is not a value. It cannot be stored, returned, or handed to someone else.
Closing that gap requires an object that represents "an operation whose result is not
yet known."

This object is called a **promise**. This lesson builds the promise's state machine, how
it is produced, and the rules of chaining; the measurement source also moves, in this
lesson, from a callback interface to a promise interface.

## The Three-State Machine

A promise is in exactly one of three states:

| State | Meaning |
|---|---|
| pending | The result is not yet known |
| fulfilled | Settled with a value |
| rejected | Settled with a reason |

Fulfilled and rejected share a common name: the **settled** state. Transitions are
one-way: a pending promise can settle, but a settled promise never returns to pending
and can never change its state.

These two constraints directly remove two of the ambiguities from the previous lesson:
the result cannot be reported twice, because the second report is ignored; and it cannot
be changed after being reported, because the state is permanent.

## Producing a Promise

A function that returns a promise starts the work and immediately returns the object
representing the result. The function given to the constructor is called the
**executor**; it receives two functions: one that settles the result, one that rejects
it.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
  D4: { delay: 15, error: "sensor fault" },
};

function requestMeasurement(station) {
  return new Promise((resolve, reject) => {
    const record = SOURCE[station];
    if (record === undefined) {
      setTimeout(() => reject(new Error(`unknown station: ${station}`)), 0);
      return;
    }
    setTimeout(() => {
      if (record.error) reject(new Error(`${station}: ${record.error}`));
      else resolve({ station, value: record.value });
    }, record.delay);
  });
}

requestMeasurement("B2").then(
  (measurement) => console.log("fulfilled:", measurement.station, measurement.value),
  (error) => console.log("rejected:", error.message),
);

requestMeasurement("D4").then(
  (measurement) => console.log("fulfilled:", measurement.station, measurement.value),
  (error) => console.log("rejected:", error.message),
);

console.log("both promises are pending");
```

```
both promises are pending
fulfilled: B2 19.8
rejected: D4: sensor fault
```

A faulty station was added to the source table; `D4` rejects on every call. This
station will be used to test error paths for the rest of the course.

The executor's body runs **synchronously**; the timer is set up before the
`requestMeasurement` call even returns. The returned value is a pending promise. This is
why the message on the last line is printed first.

One point deserves attention: the work starts **when the promise is created**. A
promise represents not "work to be started" but "the result of work that has already
started." The next lesson shows the practical consequence of this distinction.

## The Result Is Decided Exactly Once

The state machine's permanence is guaranteed by the language itself. However many times
a decision is made inside the executor, the **first** one holds.

```js
const promise = new Promise((resolve, reject) => {
  resolve("first decision");
  resolve("second decision");
  reject(new Error("late rejection"));
});

promise.then(
  (value) => console.log("value:", value),
  (error) => console.log("error:", error.message),
);
```

```
value: first decision
```

The second decision and the rejection were silently ignored. The job that the
hand-written `done` flag did in the callback pattern is done here by the language
itself.

## Callbacks Are Always a Microtask

Even if a promise has already settled, its `then` callback does not run immediately; it
is placed in the **microtask queue**. This removes the "sometimes synchronous, sometimes
asynchronous" ambiguity from the previous topic.

```js
const promise = Promise.resolve("ready value");

promise.then((value) => console.log("2 — first then:", value));
promise.then((value) => console.log("3 — second then:", value));
console.log("1 — synchronous");
queueMicrotask(() => console.log("4 — microtask registered after"));
```

```
1 — synchronous
2 — first then: ready value
3 — second then: ready value
4 — microtask registered after
```

The promise produced by `Promise.resolve` was already fulfilled the moment it was
written; even so, its callbacks ran only after all the synchronous code had run. More
than one `then` can be attached to the same promise, and all of them run, in
registration order, in the same microtask drain.

This rule has a side effect too: since promise callbacks are microtasks, a long chain of
promises calling one another can keep the task queue waiting. The starvation warning
from the previous topic applies to promises as well.

## Chaining: Every Step Produces a New Promise

A `then` call does not change the promise; it returns a **new** promise. The new
promise's result depends on what the callback returns:

- If it returns an ordinary value, the new promise fulfills with that value.
- If it returns a promise, the new promise waits for that promise's result and settles
  with it.
- If it throws, the new promise rejects with that error.

A rejection flows down the chain until it finds a callback that handles it. `catch` is
just a form of `then` that handles rejection only; `finally` runs in both cases without
changing the result.

```js
const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
};

function requestMeasurement(station) {
  return new Promise((resolve) => {
    const record = SOURCE[station];
    setTimeout(() => resolve({ station, value: record.value }), record.delay);
  });
}

requestMeasurement("B2")
  .then((measurement) => {
    console.log("1 — raw measurement:", measurement.value);
    return measurement.value;
  })
  .then((value) => {
    if (value > 25) throw new Error("value out of range");
    return Math.round(value);
  })
  .then((rounded) => {
    console.log("2 — rounded:", rounded);
    return requestMeasurement("A1");
  })
  .then((measurement) => {
    console.log("3 — chain's second measurement:", measurement.station, measurement.value);
    throw new Error("error in processing step");
  })
  .catch((error) => {
    console.log("4 — caught:", error.message);
    return "recovery value";
  })
  .then((value) => console.log("5 — chain continues after catch:", value))
  .finally(() => console.log("6 — finally runs in every case"));
```

```
1 — raw measurement: 19.8
2 — rounded: 20
3 — chain's second measurement: A1 21.4
4 — caught: error in processing step
5 — chain continues after catch: recovery value
6 — finally runs in every case
```

The third step returned a promise, and the chain waited for it to settle; the error in
the fourth step reached `catch`; since `catch` returned a value, the chain returned to
the fulfilled state and continued.

This last behavior produces a common misconception: `catch` **swallows** the error and
repairs the chain. If the error is meant to propagate, it has to be re-thrown inside
`catch`.

Any object whose result is a promise can join this chain. The standard accepts, for
this purpose, any object that has a method named `then`; such objects are called
**thenables**. When one enters the chain, it is treated exactly like a real promise.

## What Changed Compared to Callbacks

Three of the previous lesson's four gaps are directly closed. The result is now a
**value**: it can be stored, returned, put in an array. The error path can be written
for the entire chain with a single `catch`. Contract violations are prevented by the
language itself — a decision cannot be made twice, and a callback cannot be called
synchronously.

The one gap that stays open is the possibility that the operation never settles at
all. A promise stuck in pending can stay pending forever; the mechanism that solves
this is a timeout, and it will be built in a later lesson.

## Summary

- A promise is a value representing an operation whose result is not yet known; it has
  three states, and its state does not change once settled.
- The work starts when the promise is created; a promise represents the result of work
  that has already started.
- The executor runs synchronously, but `then` callbacks are always deferred to a
  microtask.
- `then` returns a new promise; returning a value continues the chain, returning a
  promise makes it wait, throwing sends a rejection down the chain.
- `catch` handles rejection and returns the chain to the fulfilled state; if the error
  is meant to continue, it has to be re-thrown.

## Next Step

Now that a single promise is a value, multiple promises can be treated as a
collection. The next lesson introduces the combinators that wait for three stations
together, take the first response, or pick the first successful response; the
coordination function hand-written with a counter and a flag in the previous topic
comes down to a single line. The same lesson also compares the combinators' different
behavior in the face of errors.
