Skip to content
AtomicReps

A promise callback runs before a timer queued earlier

A lesson from The JavaScript Event Loop: Predicting Execution Order. Play it above, or read it through below.

Level I · How the Event Loop Runs
  • The microtask queue empties before the loop takes a task

    The common read: Whichever call gets queued first also runs first, so the timer callback (queued at line 2, before the promise callback at line 3) has to log before it.

    Two queues hold work while the running code finishes. From run-to-completion: ready work waits in the task queue, and the loop takes one task per turn. A promise callback does not go to the task queue. It waits in a second queue, the microtask queue, and the loop handles that queue by a different rule.

    Every time the call stack empties, meaning the currently running code has finished, and before the loop is allowed to take its next task, it runs a microtask checkpoint: it drains the microtask queue completely, running every microtask in the order they were queued, not only the first one. Only when the microtask queue is empty does the loop take one item from the task queue for the turn.

    That is why C logged before B above. Both were queued while the script ran. The script finishes, the stack empties, the checkpoint runs, and C is the only item in the microtask queue, so C runs immediately. B is still in the task queue at that point, and B waits for its own turn after the checkpoint has emptied the microtask queue.

  • The drain empties the queue, newcomers included

    The common read: A microtask queued from inside another microtask still has to wait its turn, the same way a second task waits behind a first one, so it should log after the already-waiting timer callback, not before it.

    So far the task queue and the microtask queue share one rule: work waits its turn. The two queues differ in how much work runs per turn. The loop takes exactly one item from the task queue per turn, however many items are waiting. The checkpoint keeps running microtasks until the microtask queue is empty, and a microtask that queues another microtask while the checkpoint is running adds one more microtask to the same checkpoint.

    The snippet you just predicted, where C queues D, shows this rule. D did not exist when the stack emptied and the checkpoint began; C queued D during the drain. The checkpoint does not track when a microtask was queued. It only checks whether the queue still holds a microtask, and while the queue holds one, the loop takes nothing from the task queue.

    In short, the loop runs one item from the task queue per turn, and at each checkpoint it runs microtasks until the microtask queue is empty.

  • The checkpoint has no rule that makes it stop early

    The common read: The loop always makes room for other work eventually, so a microtask that keeps re-queuing itself still leaves gaps for a waiting timer, a repaint, or a click to get through.

    The checkpoint has one instruction, keep draining while the microtask queue is non-empty, and no instruction to stop after a while, let other work run, or check whether a task is waiting. The same rule made C log before B in the opening snippet and D log before B in the snippet where C queued D. In the spin snippet, the same rule runs without a stopping condition.

    The loop has no protection against a self-requeuing microtask. The drain rule applies to it the same way it applies to any microtask. The page stops responding without any error: the checkpoint does what it always does, and the queue is never empty, so the loop never returns to the task queue.

  • One sentence that predicts every ordering above

    The whole rule fits in one sentence you can apply to any snippet: whenever the call stack empties, not only between tasks, the loop runs microtasks until the microtask queue is empty, including microtasks queued during that run, and no task starts until the queue is empty.

    "Between tasks" is a good summary in most cases, but it misses one case. When a person clicks a button that has two listeners, the browser runs both listeners inside one task, and the stack empties after each listener returns, so a checkpoint runs between the two listeners (checkpoint-between-listeners, later on this path, covers that case).

    The checkpoint starts when the stack empties and does not stop until the microtask queue is empty. Every result in this lesson (A D C B, A C D B and the frozen tab) follows from that sentence.

    The model in this lesson is two queues and one rule for which queue the loop reads from next. The loop runs microtasks until the microtask queue is empty, and only then takes a single task. If you can draw these two queues from memory, you can predict the order of every snippet in this lesson without running it.