Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 17

Promises

Prerequisites: you should be comfortable with functions, closures, and callback-based asynchrony, and you should understand that JavaScript runs on a single thread with an event loop. If `resolve` and `reject` in a callback-style API still feel unfamiliar, read the preceding chapter first.
Level: AdvancedLength: In depth4 min read

Read at another depth

Promises

Prerequisites: you should be comfortable with functions, closures, and callback-based asynchrony, and you should understand that JavaScript runs on a single thread with an event loop. If resolve and reject in a callback-style API still feel unfamiliar, read the preceding chapter first.

The model a Promise actually implements

A Promise is a state machine over an eventual value. It has exactly three states: pending, fulfilled (with a value), or rejected (with a reason, conventionally an Error). A promise settles once, at most. Every subsequent then on a settled promise just replays the outcome — there is no "second event."

This one-way state machine is the source of most of the semantics people find surprising:

  • Handlers registered after settlement still fire.
  • A resolve call later in the executor body is ignored.
  • There is no way to inspect or cancel the underlying operation from the promise itself.
Code example in javascript
const p = new Promise((resolve) => {
  resolve(1);
  resolve(2); // ignored — already settled
});
p.then((v) => console.log(v)); // 1, even if registered much later

The executor runs synchronously the moment new Promise(...) executes. Only the handlers are asynchronous.

then chains, return values, and flattening

p.then(onFulfilled, onRejected) returns a new promise. That new promise adopts the outcome of whatever the handler does:

  • A returned value fulfills it with that value.
  • A returned promise/thenable makes the new promise wait for it and adopt its fate. This is flattening, and it is recursive — a then handler can return a then handler's promise indefinitely.
  • A thrown error rejects it.
  • If the handler is absent (or not a function), the outcome passes through unchanged.
Code example in javascript
Promise.resolve(2)
  .then((n) => n * 10)            // 20
  .then((n) => Promise.resolve(n + 1)) // waits, resolves 21
  .then((n) => { throw new Error('boom'); })
  .catch((err) => err.message);   // 'boom'

Rejecting with a non-Error value is legal but destroys stack traces and breaks instanceof checks downstream. Do it only at an API boundary you control.

Composition combinators

CombinatorSettles whenRejection behavior
`Promise.all`All fulfill, or any rejectsFails fast on first rejection
`Promise.allSettled`All settleNever rejects; returns status/value/reason objects
`Promise.race`First settlement of any kindSettles with the first outcome, fulfill or reject
`Promise.any`First fulfillmentRejects with an `AggregateError` only if all reject

Promise.any is the right tool for "try several mirrors, take whichever answers successfully"; race is wrong for that because a fast failure wins.

Code example in javascript
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
const failed = results.filter((r) => r.status === 'rejected');

Error propagation and recovery semantics

Rejection propagates down the chain until a handler catches it. Crucially, then(f) and then(f, r) differ subtly: in the two-argument form, an error thrown by f is not caught by r in the same call — r only sees rejections of the input promise. p.then(f).catch(r) catches both. Prefer catch.

Once a rejection is handled, the chain continues in the fulfilled state with the recovery value. catch is not a dead end; it is a transform.

Code example in javascript
Promise.reject(new Error('x'))
  .catch(() => 'recovered')
  .then((v) => console.log(v)); // 'recovered'

Unhandled rejections

A rejection with no handler registered by the end of the current macrotask triggers unhandledrejection (browsers) or an uncaught exception (Node, crash by default since v15). In long-lived chains — caches of promises, then attached conditionally — rejections get silently swallowed or surface much later than the bug. Attach a terminal .catch to every top-level chain.

Cancellation, deferred patterns, and leakage

Promises have no cancellation API. The common escape hatches:

  1. AbortController / AbortSignal — the operation itself honors the signal; the promise rejects with an AbortError. This is the standard for fetch.
  2. Wrapping with a race against a timer — you stop waiting, but the underlying work keeps running and may still resolve later.
Code example in javascript
function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('timeout')), ms)),
  ]);
}
// promise's work continues even if this rejects

Also beware executor leakage: an executor that never calls resolve or reject leaves a promise pending forever — the classic "forgot to resolve in a branch" bug that silently hangs await calls. Treat an unconditional settle path as a code-review invariant.

Internals: microtasks, thenables, and sync executors

Every then callback is queued as a microtask, executed after the current synchronous run-to-completion and before any macrotask (timers, I/O). Chained thens therefore drain before a setTimeout(…, 0) fires. Awaiting a native promise costs one microtask tick per link; awaiting a non-native thenable costs extra ticks because the runtime must defensively call its then twice (once with a throwaway attempt to detect a bad thenable).

Decision criteria: promises vs callbacks vs async/await

  • New public APIs: always promise-returning. Callbacks are legacy interop only.
  • Promises vs async/await: async/await is syntax over promises; choose it for sequential logic because stack traces and try/catch behave more like sync code. Choose explicit .then chains for pipeline-style transformations where each step is a named unit.
  • all vs allSettled: use all only when a single failure invalidates the whole result; otherwise allSettled plus explicit triage.

Exercise

Why does p.then(f).catch(r) behave differently from p.then(f, r) when f throws?

Show answer

In then(f, r), r handles only rejections of the input promise p; a throw inside f is unhandled at that link. In then(f).catch(r), the throw rejects the intermediate promise created by then(f), so catch(r) recovers from it.

Exercise

Promise.race([fastFail(), slowSuccess()]) rejects. Which combinator gives you slowSuccess() instead, and what does it reject with if no promise fulfills?

Show answer

Promise.any. If every promise rejects, it rejects with an AggregateError whose errors property holds all the rejection reasons.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Promises

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that