Run-to-Completion in the JavaScript Event Loop

In JavaScript, once a task starts executing on an event-loop thread, its synchronous work runs until it finishes before another task gets a turn on that thread. This guarantees atomic execution of each task, but a long-running task can prevent the browser from handling other work promptly.

A task is a unit of work processed by the event loop, such as an initial script, a timer callback, or an event callback. When the JavaScript engine begins a task, synchronous statements in that task execute in order without another task interleaving between them on the same thread.

Asynchronous APIs do not normally interrupt the currently executing JavaScript. For example, a timer callback becomes eligible to run later, but it cannot start in the middle of a currently executing function. The event loop selects another eligible task after the current task has completed.

Microtasks, such as Promise reactions, are handled differently from ordinary tasks. After a task finishes, the runtime performs a microtask checkpoint before moving on to another task, so a long chain of microtasks can also delay rendering and other tasks.

Run-to-completion makes ordinary JavaScript state changes easier to reason about because another task cannot observe the program halfway through a synchronous sequence. The tradeoff is responsiveness: expensive synchronous computation blocks other JavaScript work and can delay user interactions and rendering.

Key points

Interview questions

What does run-to-completion mean in JavaScript?

It means that once a JavaScript task begins, its synchronous execution continues until that task finishes. Another task cannot interrupt it on the same event-loop thread. This makes a sequence of synchronous state changes effectively atomic with respect to other tasks. The tradeoff is that a long task can block other work.

Why doesn't `setTimeout(fn, 0)` execute in the middle of the current function?

The timer does not directly interrupt the currently running JavaScript. It schedules a callback that becomes eligible for a later task. The current task must first finish its synchronous work. Therefore, a zero-delay timer does not mean immediate execution.

How do microtasks relate to run-to-completion?

Microtasks are not inserted into the middle of the currently executing JavaScript task. Once the task finishes, the runtime performs a microtask checkpoint and processes queued microtasks before proceeding to another task. This ordering is why Promise callbacks can run before a timer callback that was already scheduled. A large or self-perpetuating microtask queue can therefore delay other event-loop work.

How does run-to-completion affect frontend responsiveness?

Because synchronous JavaScript cannot be interrupted by another task on the same thread, expensive computation occupies that thread for its entire duration. During that time, user input, timers, and other JavaScript tasks may have to wait. Rendering opportunities can also be delayed. Developers can improve responsiveness by reducing task duration or moving suitable computation to mechanisms such as Web Workers.

Can run-to-completion be described as thread safety?

Not by itself. Run-to-completion provides an ordering guarantee for tasks executing on the same event-loop thread, but it does not make all shared state safe across workers or other concurrency mechanisms. Web Workers, shared memory, and synchronization primitives introduce additional concurrency concerns. The useful claim is that ordinary tasks on one event-loop thread do not interleave synchronously with each other.