skip to content

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%

answer

  1. drains, not runs one
  2. after each task, before the next
  3. new jobs join the same drain
  4. no snapshot of the queue
  5. whole promise chain finishes in one turn

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.

solid answer

~40 s

The microtask checkpoint is the moment, after the current task's JavaScript has run to completion and the call stack is empty, when the runtime processes queued jobs. The key word is **drains**: it does not run one microtask and move on, it keeps taking the front job until the queue is empty, and microtasks enqueued *by* those microtasks are appended to the same queue and run in the same drain. Only when nothing is left does the runtime return control and pick up the next task, such as a timer callback or an event. That is why an entire promise chain — however many `.then` hops it has — completes before a `setTimeout(fn, 0)` scheduled beforehand ever fires. The rule is one task, then the whole microtask queue, then the next task.

code

javascript · 13 lines
javascript
setTimeout(() => console.log('task'), 0);

queueMicrotask(() => {
  console.log('micro 1');
  queueMicrotask(() => console.log('micro 2 (queued during the drain)'));
});

console.log('sync');

// sync
// micro 1
// micro 2 (queued during the drain)
// task

go deeper

for a junior

Know the shape of the loop: one task runs, then all pending microtasks run, then the next task. Being able to say promise callbacks go ahead of timer callbacks is enough at this level.

for a middle

Explain the drain precisely — the engine re-checks the queue after every job, so microtasks enqueued during the drain run in the same checkpoint, and a whole promise chain finishes inside one turn.

for a senior

Demonstrate the operational consequence: the drain is unbounded and unpreemptible, so a growing pile of microtask work stretches the current turn and delays everything the runtime wanted to do next. Say how you would spot that in a profile.

for a principal

Own the tradeoff the design encodes: exhaustive draining buys atomicity for a logical operation and speed-independent ordering, at the cost of any fairness budget. Be ready to say when a system needs work expressed as tasks instead.

## The two queues A JavaScript runtime keeps work in (at least) two places. **Tasks** are coarse units of work handed to it by the host: a timer callback, an event handler, the initial evaluation of a script. **Microtasks** — the specification calls them *jobs* — are fine-grained continuations of JavaScript that is already in flight: promise reaction jobs and callbacks passed to `queueMicrotask`. The loop that connects them is deliberately asymmetric: 1. Take **one** task and run it to completion. 2. Perform a **microtask checkpoint**: drain the microtask queue to empty. 3. Go back to step 1. Step 2 is where candidates lose the point. Interviewers want to hear "drains to empty", not "runs the next one". ## What draining to empty means At the checkpoint the engine repeatedly removes the front microtask and runs it. If a microtask enqueues another microtask, the new job is appended to the *same* queue and is picked up in the *same* drain — the engine re-checks whether the queue is empty after every job. It leaves the checkpoint only when a check finds nothing left. ``` setTimeout(() => console.log('task'), 0); queueMicrotask(() => { console.log('micro 1'); queueMicrotask(() => console.log('micro 2')); }); console.log('sync'); // sync, micro 1, micro 2, task ``` `micro 2` did not exist when the drain started. It still runs before `task`, because the drain re-examines the queue rather than processing a snapshot of it. This is exactly why a promise chain resolves fully within one checkpoint. Each `.then` hop enqueues a job only when the previous one finishes, so a five-step chain is five jobs enqueued one at a time — and all five complete before the runtime looks at the task queue again. ## Where the checkpoint sits in the specification ECMAScript defines the language-level rule: a job runs only when the execution context stack is empty, so jobs never interrupt running JavaScript. Hosts formalise the surrounding loop — the HTML specification's phrase is literally "perform a microtask checkpoint", and it happens after each task, with the runtime free to take its own steps (input, painting, its own bookkeeping) only afterwards. Server runtimes have the equivalent drain between the callbacks they dispatch. The precise placement of extra checkpoints is a host concern; the invariant every host shares is that microtasks never interleave with a task and always finish before the next one begins. ## Why the design exists Two guarantees fall out of drain-to-empty. **Atomicity of a logical operation.** A promise chain is one conceptual operation split across several callbacks. Draining to empty means nothing external — no timer, no click, no I/O callback — can observe the system halfway through that chain. State assembled across `.then` hops is never seen half-built by unrelated code. **Ordering that does not depend on speed.** Because the drain is exhaustive, the relative order of microtasks depends only on the order they were enqueued, never on how long anything took. That makes promise-based code deterministic in a way callback-plus-timer code never was. ## The cost of the same property The drain has no budget and no fairness mechanism. Everything in the queue runs before the runtime regains control, so a large amount of microtask work extends the current turn: the next task waits, and so does anything the host wanted to do between tasks. A microtask is therefore the right tool for a continuation that must settle before anyone else looks, and the wrong tool for chunking a long computation — for that you need work that is genuinely a *task*, which hands control back. ## The distinctions to keep straight - **One task per turn, all microtasks per turn.** The asymmetry is the whole rule. - **Enqueued-during-drain jobs are not deferred.** They join the current drain; there is no snapshot. - **Order within the queue is FIFO**, and each job runs to completion — jobs never nest or interleave. - **A microtask cannot be pre-empted.** There is no yield point inside one; run-to-completion applies to jobs exactly as it applies to tasks. ## How to answer this out loud State the loop (one task, then drain to empty, then the next task), stress that newly enqueued microtasks join the same drain, give the two-line snippet as evidence, and name the guarantee it buys: no external work observes a promise chain mid-flight. That is a complete answer, and it is the foundation every ordering question in this area rests on.

  • A promise chain has five .then hops. How many task turns does it take to finish?
    One. Each hop enqueues the next job only when the previous handler returns, but every one of those jobs is appended to the queue that is currently being drained. All five run inside the same microtask checkpoint, so the chain completes before the runtime takes another task.
  • Does the drain give the runtime any chance to do its own work part-way through?
    No. The checkpoint runs to exhaustion, so the runtime regains control only once the queue is empty. Anything the host does between tasks — dispatching the next event, its own bookkeeping — waits for the drain to finish, which is why heavy microtask work stretches the current turn.
  • Is the microtask queue FIFO, and can one microtask interrupt another?
    It is FIFO, and no job can interrupt another. Run-to-completion applies to microtasks exactly as to tasks: the engine finishes the current job, including everything it calls synchronously, before removing the next one from the front of the queue.

saying these in an interview costs you the question

  • Says the engine runs one microtask then the next task
  • Thinks jobs queued during the drain wait for the next turn
  • Describes the drain as processing a snapshot of the queue
  • Claims each .then hop costs a separate task turn
  • Treats microtasks and timer callbacks as one queue

context