Skip to content

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?

Code for this question
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}`),);
Pick one

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
Trace of the opening question
#Callc-114 after itc-115 after itStep
1new Promise(executor), once per candidatependingpending1
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)
The probe code
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

an adult in a blue jacket stands behind a table with three boxes: a sealed, taped box; an opened box holding a smooth green ball; and a crushed, torn box. The right third of the canvas is empty.
An illustration to help it stick. The mechanism is in the sections above.

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?

Code for this question
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');
Pick one

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?

Code for this question
const stock = Promise.resolve(stockSdk.check('LMP-39'));stock.then((s) => badge.show(`${s.qty} in stock`), (e) => badge.show(`unavailable: ${e.message}`));
Pick one

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?

Code for this question
const viaResolve = new Promise((resolve) => resolve(lookup));const viaReject = new Promise((_, reject) => reject(lookup));
Pick one

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?

Code for this question
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)); }
Pick one

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?

Code for this question
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  });}
Pick one

Go deeper

Pro tips
  1. Read a promise's state in Node with util.inspect(p) (or console.log(p)). When: debugging a promise that "never resolves", or checking whether resolve locked it (step 4). Verify: it prints Promise { <pending> }, Promise { 'online' } and Promise { <rejected> Error: timeout … }. In Chrome DevTools, an expanded promise shows the same as [[PromiseState]] and [[PromiseResult]] (Status: DOC, not run in this session).
  2. Write return reject(err) in a hand-written callback wrapper. When: wrapping any (err, value) callback API. Settle-once (step 3) makes the later resolve harmless to the promise, but the code between still runs. Verify: both wrappers end rejected: Error: timeout, and only the one without return ran its cache.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 then is 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 its then carries 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.