skip to content

In JavaScript, if a promise is already fulfilled at the moment you call .then(callback) on it, does callback run immediately? Explain when it actually runs and what schedules it.

level: juniorimportance: must knowfreq 66%

answer

  1. never synchronous, even when settled
  2. a job is queued, not called
  3. microtask queue, not the timer queue
  4. runs after the current run completes
  5. state picks the branch, not the timing

basics

~20 s

No. .then never calls back synchronously: it schedules a promise reaction job on the microtask queue. The callback runs after the currently executing script or task finishes, when the engine drains microtasks at its checkpoint.

solid answer

~40 s

It never runs immediately. Calling `.then(cb)` on an already-fulfilled promise does not invoke `cb` on the current call stack; it enqueues a **promise reaction job** onto the microtask queue. The engine finishes the synchronous code it is currently running, and only when that run completes — the stack is empty and the host reaches its microtask checkpoint — does it pull the job off the queue and call `cb` with the settled value. So `Promise.resolve(1).then(v => console.log(v)); console.log(2)` prints `2` then `1`. The same deferral applies whether the promise settled a millisecond ago or an hour ago: the settled state changes *which* branch is scheduled, never *when* it is scheduled. This asynchrony is guaranteed by the specification precisely so a callback's timing does not depend on whether a value happened to be cached.

go deeper

for a junior

Be ready to say plainly that a .then callback is always deferred, even on an already-resolved promise, and that it lands on the microtask queue. Predicting the output of a two-line snippet is the usual screening form.

for a middle

Explain the mechanism: .then either stores reaction records on a pending promise or enqueues a reaction job right away when it is already settled, and that job runs once the stack unwinds at the microtask checkpoint.

for a senior

Show why this guarantee matters in production code — API design that sometimes calls back inline creates re-entrancy and half-initialised-state bugs that only appear on cached paths, and you should be able to name that failure mode from experience.

for a principal

Own the design argument: unconditional asynchrony is a contract you impose on your own callback APIs too, and you should be able to weigh its cost (an extra turn of latency per hop) against the consistency it buys across a codebase.

## The rule in one line A promise callback is **always** delivered asynchronously, on the microtask queue, even when the promise is already settled. There is no fast path that calls it inline. ## What .then actually does `p.then(onFulfilled, onRejected)` does two things. First, it creates and returns a brand-new promise — the *derived* promise — whose fate depends on what your handler returns or throws. Second, it registers your handlers with `p`, and what happens next depends on `p`'s state: - **`p` is pending** — the handlers are stored on `p` as reaction records. Nothing is queued yet. When `p` later settles, the matching reaction is enqueued as a job. - **`p` is already fulfilled or rejected** — there is nothing to wait for, so the engine immediately *enqueues* a promise reaction job. Immediately enqueues; it does not immediately call. That job is a small unit of work: "call this handler with this value, then resolve or reject the derived promise with the result". It sits in the microtask queue with every other pending job. ## Where the job runs JavaScript code runs to completion: once a function starts, the engine finishes it and everything it calls synchronously before doing anything else. Only when the call stack has fully unwound does the runtime reach the point where queued jobs may run — the **microtask checkpoint**. At that checkpoint the queue is drained and your handler is finally invoked. The practical consequence is a strict ordering with three tiers in one turn: all synchronous code first, then all microtasks (promise reactions, `queueMicrotask` callbacks), then whatever the host schedules as a separate task. ``` const p = Promise.resolve('value'); // already fulfilled p.then(v => console.log('A', v)); // job enqueued now, not run now console.log('B'); // runs first // B, then A value ``` ## Attaching handlers late still works Because a settled promise keeps its state and value forever, attaching `.then` seconds or minutes after settlement is perfectly valid — you get a fresh reaction job on the spot. This is what makes it safe to hand a promise around and let several unrelated pieces of code subscribe whenever they are ready. It also means there is no such thing as "missing" a promise result the way you can miss an event that fired before you subscribed. ## Ordering when several handlers are attached Each `.then` call on the same promise registers its own reaction. When the promise settles (or, if already settled, at each call), the jobs are enqueued in registration order and therefore run in registration order at the checkpoint. Two independent subscribers cannot interleave inside one another because each handler itself runs to completion. ## Why the specification forbids a synchronous call If a library sometimes called back inline (value cached) and sometimes asynchronously (value not yet fetched), every consumer would have to defend against both. Code after the `.then` call might or might not have run yet; state might be half-initialised when the handler fires; recursion and re-entrancy bugs appear only under the cached path, which is exactly the path that does not show up in slow-network testing. Forcing one behaviour removes an entire class of Heisenbugs, and it is why other asynchronous APIs increasingly adopt the same discipline: *never* mix synchronous and asynchronous delivery in one function. ## What this is not The microtask queue is not the queue used by `setTimeout`. Timer callbacks are tasks, and the runtime processes at most one task per turn, running the entire microtask queue in between. So a promise reaction registered on an already-settled promise runs *before* a `setTimeout(..., 0)` scheduled earlier in the same synchronous run, even though the timer was requested first. Answering "they both go in the callback queue" collapses the distinction that the question is really testing. ## Diagnosing the mistake in real code The symptom of misunderstanding this is code that reads a variable the handler is supposed to set, immediately after the `.then` call, and finds it undefined: ``` let user; getCachedUser().then(u => { user = u; }); render(user); // always undefined — render runs before the job ``` The fix is not to check whether the promise was cached; it is to move the dependent work inside the handler or `await` it, because the deferral is unconditional. Recognising that unconditional deferral — and naming the microtask queue as where the job waits — is the whole answer an interviewer is listening for.

  • If you attach two separate .then callbacks to the same already-fulfilled promise, in what order do they run?
    In registration order. Each `.then` enqueues its own reaction job, and the microtask queue is FIFO, so the first one registered runs first. Neither can interleave with the other, because each job runs to completion before the next is pulled off the queue.
  • Does a promise callback that itself schedules another microtask have to wait for the next task?
    No. Jobs queued while the engine is draining are appended to the same queue and run in the same drain, before control returns for the next task. That is why a chain of `.then` steps completes entirely before a `setTimeout(..., 0)` scheduled beforehand.
  • Why does the specification insist the callback is never invoked synchronously?
    So timing never depends on whether a value happened to be available already. An API that calls back inline when cached and later when not forces every caller to handle both orders, producing re-entrancy and half-initialised-state bugs that only appear on the fast path. Unconditional deferral removes that whole class.

saying these in an interview costs you the question

  • Says the callback fires immediately because the promise already resolved
  • Thinks .then callbacks share the queue with setTimeout callbacks
  • Claims a cached value makes the callback synchronous
  • Believes ordering depends on how fast the network responded
  • Reads the result on the line after .then and expects it set

context