skip to content

Microtask Queue

Promise reactions and queueMicrotask callbacks go to a separate, higher-priority queue that is drained completely between tasks rather than one entry per turn. This one fact decides the answer to most 'what logs first' questions you will be handed.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

9

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

open as a page

What is the microtask checkpoint in a JavaScript runtime, and what exactly does the engine do with the microtask queue when it reaches one?

level: middleimportance: must knowfreq 72%

basics

~10 s

At a microtask checkpoint the engine drains the entire microtask queue, running jobs until the queue is empty — including microtasks enqueued during the drain — before it starts the next task.

open as a page

An async function in a browser tab runs `while (true) { await Promise.resolve(); }`. Does that await give the event loop a chance to run timers, dispatch input and repaint?

level: middleimportance: must knowfreq 55%

basics

~20 s

Awaiting an already-resolved promise does not yield to timers or input. The continuation after await is queued as a microtask, so the microtask queue never empties and the tab freezes just like a synchronous while(true) loop.

open as a page

In a browser, a callback scheduled with queueMicrotask() calls queueMicrotask() on itself every time it runs. What happens to the page, and will a setTimeout callback that was already pending ever fire?

level: juniorimportance: should knowfreq 40%

basics

~20 s

The page freezes. The runtime drains the microtask queue until it is empty, and a microtask that re-enqueues itself never lets it empty, so the pending setTimeout callback, input handling and rendering never get a turn.

open as a page

In modern JavaScript, how does queueMicrotask(fn) differ from Promise.resolve().then(fn), given that both run fn at the next microtask checkpoint?

level: middleimportance: should knowfreq 42%

basics

~20 s

Both run fn at the same microtask checkpoint, in enqueue order. queueMicrotask allocates no promise and returns nothing to chain, and a throw inside it surfaces as an uncaught error rather than becoming a promise rejection that can be silently swallowed.

open as a page

A routine that drains a large work queue schedules its next step with queueMicrotask(), and the browser tab is frozen for as long as it runs. How would you restructure it so the UI stays responsive?

level: middleimportance: should knowfreq 45%

basics

~20 s

Reschedule each step onto the task queue instead of the microtask queue — setTimeout(fn, 0), a MessageChannel message, or scheduler.yield() where available — and process a batch per turn so the runtime can run timers, input and rendering between batches.

open as a page

A script makes 500 DOM changes in one synchronous loop while a MutationObserver is watching. The observer's callback runs once with 500 records rather than 500 times. Why does it behave that way, and what does that imply for code written inside the callback?

level: seniorimportance: should knowfreq 28%

basics

~20 s

MutationObserver delivers records as a microtask, not synchronously per mutation. Mutations accumulate in the observer's record queue during the synchronous run and are handed to the callback in one batch at the microtask checkpoint, so the callback sees the final state.

open as a page

After a release, one page in a web app becomes unresponsive: the tab pins a CPU core, no error appears in the console, and log lines inside setTimeout callbacks never print. How would you establish that a microtask loop is starving the event loop, and distinguish it from a plain blocking synchronous loop?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Record a performance profile of the frozen tab. Microtask starvation shows thousands of short, repeating callbacks with a shallow stack inside one task, whereas a blocking loop shows a single long frame. Pausing the debugger lands in the self-rescheduling step.

open as a page

You are designing a JavaScript library that coalesces many small updates into one flush. How do you decide between flushing at the microtask checkpoint (queueMicrotask or Promise.resolve().then) and flushing on a fresh task, and what does each choice cost?

level: principalimportance: should knowfreq 24%

basics

~20 s

Flush at the microtask checkpoint when the batch must settle before anything outside your library can observe it; flush on a task when the runtime needs control back first. Microtasks extend the current turn; tasks let unrelated work interleave.

open as a page