Skip to content
AtomicReps

Nothing interrupts a running stack

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 call stack finishes what it started

    The common read: a click that happens while a function is running is either lost entirely or runs its handler the instant the click occurs.

    The call stack is where JavaScript keeps track of which function is running and who called it. One function runs at a time. Until it returns, the only code that can run is code it calls itself. Nothing preempts it: not a timer, not a network response, not a click. This rule is called run-to-completion, and it holds for every function, all the time, with no exception for urgent work.

    A handler that is ready to run is not the same as a handler that is running. When the user clicks, the browser does not put the click handler on the stack. It puts the handler in the task queue, the list of work waiting for the stack to be free. Only when the running function returns and the stack is empty does the event loop hand the stack to the next task in line.

    That is why the click in the first question is neither lost nor run early. longSyncTask() owns the stack for its full 300ms, and done belongs to the same running script, so the click handler runs after both.

  • One task per turn

    The common read: a timer whose delay has already elapsed interrupts the running function to fire on time.

    Every piece of work that isn't already running waits as a task. A timer's callback, a click handler and the code that handles a finished network request all wait for the same condition: an empty call stack. A timer's delay sets the earliest moment its callback is ready. It says nothing about when the stack will be free.

    The event loop repeats one step: check whether the stack is empty, and if it is, take exactly one waiting task and run it to completion. It takes one task, not two and not a batch. That task then has the stack to itself until it returns, by the same run-to-completion rule.

    That is why the timer in the previous question logs last. before and after both belong to the function that is running; the timer's callback could not interrupt it, however overdue the callback was.

    The HTML standard lets a browser keep more than one task queue and choose which queue to serve next, so a click can run before a timer callback that was queued earlier. Tasks of the same kind, such as two clicks, still run in the order they were queued.

  • What run-to-completion guarantees, and what it costs

    Run-to-completion is the reason a function never has to guard against being interrupted halfway through. No other code can see a half-updated object or a DOM halfway through a rewrite, because no other code runs until the current function is done changing it. That guarantee is why most JavaScript code needs no locks or mutexes.

    The cost comes from the same rule. A long-running function keeps that guarantee by occupying the stack, and while it does, nothing else runs: not the next task and not a click handler. A 500ms loop is 500ms during which the page can't respond to anything. (If a tab has ever gone grey and stopped responding while you waited, this rule was the cause, not a bug in the browser.)

    This lesson does not cover everything about queued work. A later lesson in this course looks at a page that freezes even though no single callback runs for long: each callback queues more work before it returns, and that queue never empties. That kind of freeze is a different failure mode from a single long loop, and it needs a queue this lesson hasn't introduced yet.

  • One question answers every case in this lesson

    Every scenario in this lesson reduces to one question: is the call stack empty right now? While the answer is no, nothing else runs: not a timer past its delay, not a click, not any other queued task. The moment the answer becomes yes, the event loop takes exactly one waiting task and the same rule applies to that task: it runs on the stack until it returns.

    Nothing in this lesson depends on what kind of task is waiting. A timer callback, a click handler and every other task follow the same rule: none of them runs until the stack is empty. The next lesson adds the microtask queue. Microtasks also wait for an empty stack, but they run before the event loop takes the next task.