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.
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
- The executor resolves with "paid". The promise is now fulfilled; the later reject and resolve are ignored.
- Each then() registers a reaction and returns a new, pending promise.
- The first reaction runs as a microtask, prints "paid" and returns 4, which fulfills p1.
- The next reaction prints 4 and throws. p2 is rejected.
- 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?
Resolving a with another promise does not settle it at once. The engine schedules a job to call p.then, and then one more tick for p’s result to reach a. So “a” comes after 1 and 2. It is tempting to think resolving with a finished promise is instant.
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.”
Follow-up Does then() run synchronously if the promise is already fulfilled?
then() runs first.Follow-up What is a thenable?
then method. Promises treat it like a promise and adopt its result.Follow-up What does returning a promise from a then callback do?
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, andrejectrejects it.trustedsettles with the first value when a thenable calls back twice.trustedrejects when the thenable’sthenmethod throws before calling back.
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
then.then callbacks without an error handler until a catch or second argument handles it.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
then on a settled promise queues the job at once.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
value.then in a separate job, which is why it costs two extra ticks.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.