skip to content

What does this script print, and why? setTimeout(() => { console.log('timer 1'); Promise.resolve().then(() => console.log('microtask in timer 1')); }, 0); setTimeout(() => console.log('timer 2'), 0);

level: middleimportance: should knowfreq 55%

answer

  1. the checkpoint is not once per script
  2. every callback ends with an empty stack
  3. started work finishes before the next callback
  4. registration order does not group across queues

basics

~10 s

It prints timer 1, microtask in timer 1, timer 2. A promise callback queued inside a timer callback runs as soon as that callback returns, before the runtime picks up the next timer callback.

solid answer

~40 s

The output is `timer 1`, then `microtask in timer 1`, then `timer 2`. Both timer callbacks are queued as separate tasks with the same delay, so they run in registration order. The important part is what happens between them: when the first timer callback returns, the call stack empties, and the runtime drains the microtask queue before taking the next task. The promise reaction queued inside that callback therefore runs immediately after it — not after `timer 2`. The generalization is that the microtask checkpoint is not a once-per-script event: it happens after each task, so promise work started inside a callback completes before any other queued callback gets a turn.

go deeper

for a junior

Know that a promise callback created inside a timer callback runs right after that callback finishes, before the next timer callback. State the three log lines in order.

for a middle

Explain that the microtask checkpoint happens every time the call stack empties, not only at the end of the script, and that microtasks queued during a drain join the same drain.

for a senior

Show where you exploit this in real code — batching follow-up work at the end of a unit of work — and trace the mirror-image case where a timer scheduled from a microtask lands behind an earlier timer.

for a principal

Own the risk: microtask work inside a hot callback delays every other queued callback, so batching bought with microtasks trades fairness for consistency. Be able to say when that trade is wrong and the work belongs on a later task instead.

## The output ``` timer 1 microtask in timer 1 timer 2 ``` ## Why both timers run in that order The two `setTimeout` calls register two independent callbacks with the same delay. They are enqueued in registration order, and the runtime takes one task per turn, so `timer 1`'s callback runs first and `timer 2`'s second. Nothing subtle there — the subtlety is what fits *between* them. ## The checkpoint happens after every task, not once per script Many people internalize the basic puzzle as "synchronous code, then promises, then timers" and stop there. That phrasing is a special case of the real rule: **whenever the call stack empties, the microtask queue is drained completely before the next task is taken.** The end of the top-level script is only the first time the stack empties; the end of every callback is another. So the trace is: 1. Task A runs the first timer callback. It prints `timer 1` and enqueues a promise reaction. 2. The callback returns; the stack is empty. Checkpoint: drain microtasks, printing `microtask in timer 1`. 3. The loop takes the next task — the second timer callback — printing `timer 2`. ## Why this matters beyond the whiteboard This is the rule that makes promise-based code inside a callback behave predictably. If you write: ```js elementHandler(() => { updateModel(); Promise.resolve().then(flushChanges); }); ``` you can rely on `flushChanges` running before any other queued callback observes the model — the checkpoint acts as a "finish what you started" boundary at the end of each unit of work. Frameworks and libraries use exactly this property to batch: do the cheap synchronous mutations now, queue a microtask, and let it perform the single expensive follow-up before anything else runs. It also means a chain started inside a callback completes entirely there: ```js setTimeout(() => { Promise.resolve() .then(() => console.log('a')) .then(() => console.log('b')); }, 0); setTimeout(() => console.log('c'), 0); ``` prints `a`, `b`, `c`. The second `.then` is queued while the first microtask is running, and microtasks queued during a drain are part of the same drain. Only genuinely asynchronous waits — another timer, real I/O — push work past the boundary. ## The mirror-image variant Swap the nesting and the reasoning inverts: ```js Promise.resolve().then(() => { console.log('microtask'); setTimeout(() => console.log('timer from microtask'), 0); }); setTimeout(() => console.log('outer timer'), 0); ``` Here the microtask runs first (before any task), but the timer it schedules is queued *behind* the timer that was already registered, so the output is `microtask`, `outer timer`, `timer from microtask`. Tasks queued later go to the back of the line; microtasks queued later still run in the current drain. Being able to explain both directions is what separates a memorized answer from an understood one. ## Tracing method Keep the three columns, but redraw the microtask column after every task rather than once. Each time a callback returns, ask: did it queue microtasks? Drain them before moving to the next task row. That single discipline handles almost every nested variant an interviewer can construct. ## What a weak answer looks like The common wrong output is `timer 1`, `timer 2`, `microtask in timer 1`, produced by reasoning "all the timers were registered first, so they all run before any promise work". Registration order does not group callbacks across queues; the checkpoint boundary does.

  • How does the output change if the first timer's callback schedules another setTimeout instead of a promise?
    You get `timer 1`, `timer 2`, then the new one. A task queued from inside a callback goes to the back of the task queue, behind the timer that was already registered, so it cannot jump ahead — the opposite of what a queued microtask does.
  • If the first timer's callback starts a two-step .then chain, does the second step also run before timer 2?
    Yes. The second reaction is queued while the first microtask is running, and microtasks queued during a drain are part of that same drain. The queue is emptied, not sampled once, so an entire synchronous-settling chain completes before the next task.
  • Why do libraries queue a microtask at the end of a callback rather than doing the work inline?
    To batch. Several synchronous mutations can each mark work as needed, but the queued microtask runs once, after the current callback finishes, and does the expensive follow-up a single time — while still completing before any other queued callback can observe an inconsistent state.

saying these in an interview costs you the question

  • Says all registered timers run before any promise callback
  • Thinks the microtask drain only happens once, after the script
  • Claims a setTimeout scheduled inside a callback runs before earlier timers
  • Says the microtask must wait because it was queued last

context