ESC

Type to search the knowledge base.

Promises

Settlement, chaining, errors, Promise API helpers, and how promises plug into the microtask queue — without cargo-cult async.

intermediate3 min read
  • promises
  • async
  • javascript

A Promise is an object for a value that may arrive later. It starts pending, then fulfills or rejects once. Handlers run as microtasks — that’s why they interleave with timers the way they do (see Event Loop).

Callbacks still work. Promises make composition and error paths less of a pyramid of doom.

The contract

const p = new Promise((resolve, reject) => {
  // executor runs sync
  setTimeout(() => resolve('done'), 100);
});

p.then((value) => console.log(value));
  • resolve(x) → fulfilled with x (unless x is a thenable — then it adopts that).
  • reject(err) → rejected.
  • Calling resolve/reject again is a no-op.

.then, .catch, .finally always return a new promise. That’s chaining.

Chaining (the useful part)

fetch('/api/user')
  .then((res) => {
    if (!res.ok) throw new Error(String(res.status));
    return res.json(); // return a promise → next then waits
  })
  .then((user) => user.name)
  .catch((err) => {
    console.error(err);
    return 'anonymous';
  })
  .finally(() => {
    // cleanup; does not change fulfillment value unless it throws
  });

Return a value → next then gets it.
Return a promise → next then waits.
Throw or return a rejected promise → control jumps to the next catch.

Errors fall down the chain

One catch at the end is often enough for a linear pipeline. If you recover inside catch by returning a normal value, the chain continues fulfilled. If you rethrow, rejection continues.

Promise.resolve()
  .then(() => {
    throw new Error('boom');
  })
  .then(() => console.log('skipped'))
  .catch((e) => console.log('handled', e.message));

Promise API (know these cold)

Helper Meaning
Promise.all Fail fast on first rejection; all must fulfill
Promise.allSettled Wait for all; get {status, value|reason}[]
Promise.race First settle wins (fulfill or reject)
Promise.any First fulfillment wins; rejects only if all reject
Promise.resolve / reject Wrap a value / reason
// Parallel work when order of completion doesn’t matter for *starting*
const [a, b] = await Promise.all([fetchA(), fetchB()]);

// Don’t use all if one failure should not cancel the others
const results = await Promise.allSettled([fetchA(), fetchB()]);

Interview trap: Promise.all rejects on the first failure and does not cancel the other fetches — they still run unless you abort them yourself (AbortController).

async/await

async function load() {
  try {
    const res = await fetch('/api');
    return await res.json();
  } catch (e) {
    // rejects from await land here
    throw e;
  }
}
// load() always returns a Promise

Sequential await is intentional when B needs A. For independent work, start both first:

const pa = fetchA();
const pb = fetchB();
const [a, b] = await Promise.all([pa, pb]);

Cancellation (reality check)

Promises don’t cancel. Fetch does, with signals:

const ac = new AbortController();
const p = fetch('/slow', { signal: ac.signal });
ac.abort(); // rejects the fetch with AbortError

Mental model for production

  • Prefer async/await for linear control flow; keep .then when mapping streams of promises.
  • Always decide error policy: fail fast (all) vs best-effort (allSettled).
  • Don’t forget the event loop: heavy work after await still blocks the main thread.

Further reading

Related guides