Engrain by ByteRush

JS.11.01 Mid-level· 11 minCore — asked often

Promise internals: states, reactions and resolution

What a promise really is: three states, settling once, reaction jobs, then() returning a new promise, errors that skip ahead, and why resolving with a promise costs extra ticks.

Comes up in: JavaScript, Coding

Learn#

Divya’s checkout shows “Payment failed” after a successful payment. Her code called resolve and then, by mistake, reject. Which one wins?

A promise is in one of three states: pending, fulfilled or rejected. It settles once, to fulfilled or rejected, and never changes again.

const order = new Promise((resolve, reject) => {
  resolve("paid");
  reject(new Error("too late"));
  resolve("ignored");
});
order
  .then((v) => { console.log(v); return v.length; })
  .then((n) => { console.log(n); throw new Error("bad step"); })
  .then(() => console.log("skipped"))
  .catch((e) => { console.log("caught:", e.message); return "recovered"; })
  .then((v) => console.log(v));

Output (verified)

paid
4
caught: bad step
recovered

The first call won. Divya’s real bug was elsewhere; her promise had stayed “paid”.

Every then() stores a reaction and returns a new promise. When the reaction runs, its return value settles that new promise; a thrown error rejects it.

A promise chain, step by stepStep 1 of 5

State

  • order: fulfilled ("paid")

Microtasks

    empty

    Console

      empty

      The executor resolves with "paid". The promise is now fulfilled; the later reject and resolve are ignored.

      Read the steps as text
      1. The executor resolves with "paid". The promise is now fulfilled; the later reject and resolve are ignored.
      2. Each then() registers a reaction and returns a new, pending promise.
      3. The first reaction runs as a microtask, prints "paid" and returns 4, which fulfills p1.
      4. The next reaction prints 4 and throws. p2 is rejected.
      5. The rejection skips the then() that has no error handler. catch() handles it and returns a value, so the chain is fulfilled again.

      Predict#

      Predict first

      p = Promise.resolve(). a = new Promise(r => r(p)). Then a.then(log 'a') and p.then(log 1).then(log 2).then(log 3). What order?

      Interview angle#

      Say it like this — about 30 seconds

      “A promise starts pending and settles once, to fulfilled or rejected; later calls are ignored. then registers a reaction and returns a new promise, settled by what the callback returns or throws. Reactions run as microtasks, even if the promise is already settled. A rejection skips ahead to the next handler for errors. Resolving with a thenable adopts its state, which costs extra microtask ticks.”

      Stays on your device. Nothing is uploaded.
      Follow-up Does then() run synchronously if the promise is already fulfilled?
      No. The reaction is always queued as a microtask, so code after then() runs first.
      Follow-up What is a thenable?
      Any object with a then method. Promises treat it like a promise and adopt its result.
      Follow-up What does returning a promise from a then callback do?
      The new promise waits for that promise and copies its result. This is how chains run async steps in order.

      Practice#

      On your own

      Write withResolvers and trusted. Both rely on one rule: a promise settles once.

      Done when

      • withResolvers().resolve(5) fulfills its promise with 5, and reject rejects it.
      • trusted settles with the first value when a thenable calls back twice.
      • trusted rejects when the thenable’s then method throws before calling back.
      Your solution
      Ctrl/⌘ + Enter

      Remember#

      Try to answer each card before you open it. Then add them to your reviews.

      What are the three states of a promise?

      Pending, fulfilled and rejected.

      What happens if you call resolve and then reject?

      Nothing changes. The promise already settled; later calls are ignored.

      What does then() return?

      A new promise, settled by what its callback returns or throws.

      Why does resolving with a promise take extra ticks?

      The engine must call its then in a job, then wait for its result.

      Go deeper

      Four layers. Stop at the one your interview needs; each ends with a quick check.

      The core ideaEvery round

      A return value passes to the next then.
      A thrown error rejects the next promise, and the chain skips then callbacks without an error handler until a catch or second argument handles it.
      Handling the error returns the chain to fulfilled.

      Check yourself: What does a catch() that returns a value do to the chain?

      It fulfills the next promise, so the chain continues normally.

      How it works underneathSenior rounds

      A promise keeps a list of reactions. When it settles, it queues one microtask, a reaction job, for each Source: ECMA-262: Promise Objects. Calling then on a settled promise queues the job at once.
      The resolve function given to the executor has an “already resolved” flag; that flag is why it works once.

      Check yourself: When is a reaction job queued?

      When the promise settles, or at once if it is already settled when then() is called.

      Edge cases and trapsWhere people slip

      The promise resolution procedure checks the value: a thenable is adopted, anything else fulfills. Adoption calls value.then in a separate job, which is why it costs two extra ticks.
      Resolving a promise with itself rejects with a TypeError.

      Check yourself: What happens if you resolve a promise with itself?

      It rejects with a TypeError (a chaining cycle).

      In productionStaff-level answers

      Wrap foreign thenables with Promise.resolve(x): you get a real promise with the settle-once rule. Promise.withResolvers() replaces the old “deferred” pattern Source: MDN: Promise.withResolvers(). JS.15.04 builds a full promise from these rules.

      Check yourself: Why wrap a third-party thenable with Promise.resolve?

      It turns it into a real promise that settles once and runs callbacks as microtasks.

      Finished the five steps? Mark it, and its cards join your reviews.

      Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.