skip to content

What are the three states of a JavaScript promise, and what happens if the executor passed to new Promise() calls resolve() and then calls reject() a moment later?

level: juniorimportance: must knowfreq 78%

answer

  1. a one-way state machine
  2. pending is the only non-final state
  3. later settle calls change nothing
  4. first call wins, silently ignored after

basics

~20 s

A promise is pending, fulfilled, or rejected. The first resolve() or reject() settles it permanently; every later call is silently ignored. So a promise that fulfilled can never become rejected, and its value can never change.

solid answer

~40 s

A promise starts **pending** and can move exactly once to **fulfilled** (it has a value) or **rejected** (it has a reason). Fulfilled and rejected together are called *settled*. The transition is one-way and one-time: the promise records that it has been resolved on the very first `resolve()` or `reject()` call, and every subsequent call from the executor is a no-op — no error, no warning, no state change. So in an executor that calls `resolve('a')` and then `reject(new Error('b'))`, the promise is fulfilled with `'a'` and the rejection simply vanishes. The same immutability is why a promise cannot be reset to pending or re-run: it models a single eventual *result*, not a restartable task. If you need the work again, call the function that produced the promise again.

go deeper

for a junior

Name the three states — pending, fulfilled, rejected — and say plainly that the first resolve() or reject() settles the promise for good and later calls do nothing.

for a middle

Explain the already-resolved flag: why the second settlement is silently discarded rather than an error, and why an executor that throws after resolving stays fulfilled.

for a senior

Show what settle-once buys production code — a shared promise cannot deliver two conflicting outcomes — and point out that it also means a stored rejected promise never heals on its own.

for a principal

Frame the tradeoff: immutability makes promises safe to cache and share but makes them useless as a retry unit, so an API must decide whether it hands out results or hands out functions that produce them.

## The three states Every promise object carries an internal state that is exactly one of three values: - **pending** — the initial state; no result yet. - **fulfilled** — the operation produced a value, which the promise now stores. - **rejected** — the operation failed and the promise stores a *reason* (conventionally an `Error`, though any value is allowed). *Fulfilled* and *rejected* are together called **settled**. Pending is the only non-final state, and the only legal transitions are `pending -> fulfilled` and `pending -> rejected`. There is no `fulfilled -> rejected` transition, no way back to pending, and no way to change the stored value once it is there. ## Where the transition is triggered With the constructor form you supply an *executor* function, which the `Promise` constructor calls immediately with two functions of its own: ```js const p = new Promise((resolve, reject) => { // resolve(value) and reject(reason) are the only handles on this promise's state }); ``` One subtlety of naming: the first function is called `resolve`, not `fulfill`. Handing it an ordinary value fulfills the promise with that value straight away. Handing it another promise or thenable makes this promise *track* that one, so it stays pending until the inner one settles — the promise is then "resolved" without yet being settled. For the state machine what matters is that the outcome is locked in either way. ## Settle-once immutability The first call to `resolve` or `reject` flips an internal already-resolved flag. Every later call — with either function, with any argument — does nothing at all. It does not throw, it does not log, and it does not overwrite the result: ```js const p = new Promise((resolve, reject) => { resolve('first'); reject(new Error('ignored')); resolve('also ignored'); }); // p is fulfilled with 'first' ``` That silence is deliberate but it is also a real debugging hazard: an executor with two code paths that both settle looks correct and behaves correctly only because the second settlement is thrown away. The same flag explains an easy-to-miss rule about throwing. If the executor throws *before* settling, the promise is rejected with the thrown value: ```js const p = new Promise(() => { throw new Error('boom'); // p is rejected with this Error }); ``` But if it throws *after* `resolve()` has already run, the promise is already resolved, so the exception is discarded and the promise stays fulfilled. ## Subscribing after settlement A settled promise keeps its value or reason for as long as the object is alive. Attaching a handler later still works — the handler is called with the stored result: ```js const done = Promise.resolve(42); setTimeout(() => done.then(v => console.log(v)), 1000); // logs 42 ``` This is one of the sharpest differences from event-listener style APIs, where a listener registered after the event fired simply misses it. A promise has no "too late". Handlers are still invoked asynchronously — never synchronously inside the `.then()` call itself — but the exact scheduling is a separate concern from the state machine. Because the result is retained, any number of independent subscribers can attach at any time and all observe the same value. Nothing consumes or drains it. ## Why one-time settling is the point Settle-once gives callers guarantees that callback APIs famously fail to give: your success handler cannot be invoked twice, you cannot be told both success and failure, and you cannot be handed a result that mutates afterwards. That makes a promise safe to store, share, and pass around — it is an immutable record of one outcome. The flip side is the trap: **a promise is a result, not a task**. There is no `restart()`, no `reset()`, no way to make a rejected promise try again. Retry logic must re-invoke the *function* that creates the promise, producing a new promise each time. ## Reading the state The language exposes no standard synchronous way to ask a promise what state it is in — you learn the outcome only by attaching a handler and waiting. Debuggers and runtime inspection tooling can display the state out of band, but there is no API in the language for it, and code that appears to "check if a promise is done" is really tracking a flag it set itself in a handler.

  • If a promise fulfilled ten seconds ago, what happens when I attach a .then handler to it now?
    The handler still runs, with the value the promise stored when it settled. A promise retains its result for the lifetime of the object, so there is no "missed it" case as there is with event listeners. The handler is invoked asynchronously rather than inline, but it is definitely invoked.
  • Can you synchronously check whether a promise has settled from ordinary JavaScript?
    No — the language exposes no standard API for reading a promise's internal state. The only way to observe the outcome is to attach a handler and wait. Code that appears to check "is it done" is really consulting a boolean that the code itself set inside a handler; debuggers show state through out-of-band inspection, not a language feature.
  • An executor throws an exception after it has already called resolve(). What is the promise's final state?
    Fulfilled, with the value passed to resolve(). The constructor catches an executor exception and routes it to reject, but reject is a no-op once the promise is already resolved, so the exception is discarded. That is a genuine risk: a real bug after the resolve() call disappears without a trace.

saying these in an interview costs you the question

  • Says a settled promise can be reset back to pending
  • Thinks a second resolve() call throws an error
  • Believes reject() after resolve() switches the state to rejected
  • Claims .then on an already-settled promise never fires
  • Treats a promise as a task that can be re-run

context