Promise states: pending, fulfilled, rejected
A promise holds one result: the first resolve or reject wins, a thenable is followed, and reactions always run later.
Predict first
Before you read: predict
ExamHall's proctor console shows one status tile per candidate. getStatus wraps the legacy feed client:
The client calls the callback twice for both candidates, in opposite orders. For c-114 it passes new Error('timeout'), then, after its own internal retry, (null, 'online'). For c-115 it passes (null, 'online'), then a stale new Error('timeout').
What do the two tiles show?
function getStatus(id) { return new Promise((resolve, reject) => { feed.status(id, (err, status) => { if (err) reject(err); resolve(status); }); });}for (const id of ['c-114', 'c-115']) getStatus(id).then( (s) => tiles[id].show(String(s)), (e) => tiles[id].show(`error: ${e.message}`),);The 30-second version
A promise starts pending and settles once: fulfilled with a value or rejected with a reason. The first resolve or reject call wins; later calls, and a later throw, are ignored silently. Resolving with a thenable only locks it to follow that thenable: resolved, still pending, possibly rejected later. A reaction on a settled promise runs later, never inside then.
A promise holds one result: the first resolve or reject wins, a thenable is followed, and reactions always run later.
Step through the code
Trace of the opening question
Play options
Ready: press Play or step with Next frame
Row 1 of 4 · # 1
Starting row · computed by comparing each row with the one before
- #: 1
- Call:
new Promise(executor), once per candidate - c-114 after it: pending
- c-115 after it: pending
Rule · step 1
new Promise(executor) creates a pending promise and calls the executor at once with two functions: resolve and reject.
- #
- 1
- Call
new Promise(executor), once per candidate- c-114 after it
- pending
- c-115 after it
- pending
- Step
- 1
| # | Call | c-114 after it | c-115 after it | Step |
|---|---|---|---|---|
| 1 | new Promise(executor), once per candidate | pending | pending | 1 |
| 2 (changed) | first callback: c-114 reject(err), then resolve(undefined); c-115 resolve('online') (changed) | rejected: timeout (the resolve is ignored) (changed) | fulfilled: online (changed) | 2, 3 |
| 3 (changed) | second callback: c-114 resolve('online'); c-115 reject(err), then resolve(undefined) (changed) | ignored (changed) | ignored (changed) | 3 |
| 4 (changed) | .then(…) on each settled promise, then the queued reactions run (changed) | tile: error: timeout (changed) | tile: online (changed) | 7, 8 |
Space plays or pauses · ←/→ one row · Home/End first/last
Rows come from the pack's own trace table.
The probe code (rows are not linked to code lines)
function getStatus(id) { return new Promise((resolve, reject) => { feed.status(id, (err, status) => { if (err) reject(err); resolve(status); }); });}for (const id of ['c-114', 'c-115']) getStatus(id).then( (s) => tiles[id].show(String(s)), (e) => tiles[id].show(`error: ${e.message}`),);How it works
Step 1 of 10
new Promise(executor) creates a pending promise and calls the executor at once with two functions: resolve and reject.See it run
In the kitchen
- a new game's card reads "in play" → pending (step 1)
Analogy: A correspondence-chess results secretary
Games are played by post over months. The tournament secretary keeps one result card per game.
| At the secretary's desk | In the code |
|---|---|
| a new game's card reads "in play" | pending (step 1) |
| the secretary inks "won" or "lost" on it | fulfilled or rejected: settled (step 2) |
| a later letter claiming another result is filed unread; nobody is told it was refused | later calls ignored, silently (step 3) |
| "this game's result follows the playoff": the card is committed but still reads "in play", and may end "lost" | resolve(thenable): resolved, still pending (steps 4–5) |
| the playoff's own letters count only the first time too | a fresh once-only pair (step 6) |
| a club asking about an inked result gets it in the next bulletin, never read out mid-sentence | reaction queued, never inside then (step 7) |
| every club that asks, early or late, gets the same inked result | the settled value is kept (step 8) |
In real terms: a promise records one outcome; the first resolve or reject decides it, and every reaction later receives that outcome.
Breaks down when: a secretary can correct a clerical error; a promise has no correction. Wrong inference: "the secretary may accept a better-attested result later." Nothing ever does.
Contrasting case: a club noticeboard where each new notice covers the last. Many values over time are an event stream or a variable, not one promise.
Examples, run for real
States are read with Node's util.inspect, shown as pending, fulfilled: … or rejected: … (lib/util.mjs, pro tip 1).
4.1 ExamHall: one promise per attempt
EXH-1800: tiles showed error: timeout for candidates who were online, because the client's internal retry called the same callback a second time. MR !2000 turns that retry off and retries with a new promise.
function getStatus(id) {
return new Promise((resolve, reject) => {
feed.status(id, { retries: 0 }, (err, status) => (err ? reject(err) : resolve(status)));
});
}
const a1 = getStatus('c-114'); // the feed answers each call once, 200 ms later (virtual clock)
a1.then(undefined, (e) => tile.push(`retrying after ${e.message}`));
const a2 = getStatus('c-114'); // created after a1 has settled
t=0 attempt 1 created: pending
t=200 attempt 1 after its answer: rejected: Error: timeout
t=200 attempt 2 created: pending
t=400 attempt 2 after its answer: fulfilled: 'online'
t=400 attempt 1 still: rejected: Error: timeout
tile log: retrying after timeout · tile: online
Each attempt starts pending and settles once (steps 1–3). Attempt 1 stays rejected for good, so a retry needs a new promise.
4.2 Proof domain, catalog: a price SDK's thenable
const price = new Promise((resolve) => {
resolve(priceSdk.quote('LMP-39')); // an object with then(), which calls onFail
log.push('executor returned');
});
right after the executor: pending
after the jobs ran: rejected: Error: no price for LMP-39
log: executor returned · sdk then() called · price hidden: no price for LMP-39
resolve was called, yet the promise is still pending: resolved is not fulfilled (step 4). The SDK's then runs only after the executor returns, and the promise ends rejected (steps 5–6).
4.3 ExamHall: tiles that attach late
The console's connected promise settled at startup; three tiles attach reactions afterwards.
const connected = Promise.resolve('wss-eu-1');
let seen = 0;
for (const tile of ['c-201', 'c-202', 'c-203']) connected.then((url) => { seen++; console.log(`${tile} sees ${url}`); });
console.log(`after the loop: seen = ${seen}`);
after the loop: seen = 0
c-201 sees wss-eu-1
c-202 sees wss-eu-1
c-203 sees wss-eu-1
after the jobs ran: seen = 3
No reaction ran inside its then call (step 7), and every tile received the same value (step 8). Their order against other queued jobs belongs to the event-loop lesson.
Picture it

Try it
Answer first, then check. Nothing is saved.
Question 1 of 5
A product-detail page reads its product from a legacy cache SDK. On a hit, legacyCache.get returns a thenable, not a promise: its then(onOk) calls onOk(product) straight away, where product.name is 'Desk lamp'.
What does the log show?
const hit = legacyCache.get('LMP-39');const product = new Promise((resolve) => hit.then(resolve));hit.then((p) => log(`a: ${p.name}`));product.then((p) => log(`b: ${p.name}`));log('render');Question 2 of 5
A legacy stock SDK returns a thenable, not a promise. Its then(onOk, onFail) first calls onFail(new Error('warehouse offline')) for the primary warehouse, then onOk({ qty: 3 }) for the fallback.
What does the badge show?
const stock = Promise.resolve(stockSdk.check('LMP-39'));stock.then((s) => badge.show(`${s.qty} in stock`), (e) => badge.show(`unavailable: ${e.message}`));Question 3 of 5
A category page's price helper builds two promises from lookup, a price-lookup promise still in flight:
Once lookup has fulfilled with 39, which describes the two?
const viaResolve = new Promise((resolve) => resolve(lookup));const viaReject = new Promise((_, reject) => reject(lookup));Question 4 of 5
A sports app's match card reads the score from one module-level promise:
After goals make it 1–0, 2–0 and 2–1, a fan opens the match card and sees 0–0.
Which change makes a card opened now show 2–1?
let resolveScore, rejectScore;let latestScore = new Promise((res, rej) => { resolveScore = res; rejectScore = rej; });resolveScore('0–0'); // kick-offfeed.on('score', (s) => resolveScore(s));function openMatchCard() { latestScore.then((s) => card.show(s)); }Question 5 of 5
A checkout authorises through a primary card gateway and fails over to a backup:
In an outage the primary emits 'down' within 0.1 s, and 4 s later rejects its pending calls with Error('primary unreachable'). The backup is up and approves in 0.3 s. Yet every checkout in the outage fails with primary unreachable after 4 s.
Which is the bug?
function authorize(order) { return new Promise((resolve) => { resolve(primary.authorize(order)); // the primary gateway's promise primary.once('down', () => resolve(backup.authorize(order))); // fail over during an outage });}Go deeper
Pro tips
- Read a promise's state in Node with
util.inspect(p)(orconsole.log(p)). When: debugging a promise that "never resolves", or checking whetherresolvelocked it (step 4). Verify: it printsPromise { <pending> },Promise { 'online' }andPromise { <rejected> Error: timeout … }. In Chrome DevTools, an expanded promise shows the same as[[PromiseState]]and[[PromiseResult]](Status: DOC, not run in this session). - Write
return reject(err)in a hand-written callback wrapper. When: wrapping any(err, value)callback API. Settle-once (step 3) makes the laterresolveharmless to the promise, but the code between still runs. Verify: both wrappers endrejected: Error: timeout, and only the one withoutreturnran itscache.write(status)side effect.
Source
- Quoted: "A promise is resolved if it is settled or if it has been “locked in” to match the state of another promise. Attempting to resolve or reject a resolved promise has no effect." and "A resolved promise may be pending, fulfilled or rejected." (steps 3–5). §27.5.1.3 CreateResolvingFunctions: each resolving function returns at once once the pair has been used, and a thenable's
thenis called through NewPromiseResolveThenableJob, queued by HostEnqueuePromiseJob, with a fresh pair (step 6). §27.5.5.4.1 PerformPromiseThen: on a fulfilled or rejected promise it performs HostEnqueuePromiseJob (step 7). §27.5.3.1 Promise ( executor ): an abrupt completion of the executor calls the pair's reject (step 3's throw). In the same CreateResolvingFunctions, the reject steps are only "Perform RejectPromise ( promise , reason )", with none of the resolve steps'Get ( resolution , "then" )check (step 9). "A thenable is an object obj for which Get ( obj , "then" ) returns a function object": nothing else is required of it, so itsthencarries none of PerformPromiseThen's queueing (step 10). Valid as of 2026-09-29. - MDN, Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise · fetched 2026-09-29, saved as
sources/mdn-promise.txt. Quoted: "If the promise has already been fulfilled or rejected when a corresponding handler is attached, the handler will be called, so there is no race condition between an asynchronous operation completing and its handlers being attached." and "resolved promises can be pending or rejected as well" (steps 4, 8). Valid as of 2026-09-29. - The section numbers are the ones printed in the fetched 2027 draft; they shift between editions. Quote the text, not the number.
Checked against the tools and docs as of 2026-09-29.