JavaScript event loop

One thread runs one task at a time: microtasks go before the next task, and a frame only fits between two.

Assembling the machine…

Listing

  1. console.log("A");
  2. setTimeout(() => {
  3. console.log("B");
  4. }, 0);
  5. Promise.resolve().then(() => {
  6. console.log("C");
  7. });
  8. console.log("D");
Console—

Step 1 / 8

The script runs as one task

The script logs A, sets a 0 ms timer for B, queues C on a resolved promise and logs D. All of it is one task: the event loop takes it from the task queue and runs it on the call stack.

Event log · 1
  1. 0.00 msTASKrun script

The concept

One thread runs one task at a time: microtasks go before the next task, and a frame only fits between two.

The JavaScript event loop is how a browser runs a page's code on a single thread: it takes one task from its task queues, runs it to the end on the call stack, empties the microtask queue, and renders a frame when one is due, then starts again.

A task is a unit of work the browser schedules: running a script, dispatching a click, calling back a timer. Browsers keep several task queues and choose which one to serve next, but they run one task at a time, and nothing interrupts the code it starts until the call stack is empty.

A microtask is a job the running code queues to run right after it: a promise's then callback, the rest of an async function after await, queueMicrotask. Once the stack is empty, at the end of every task and after each callback the browser calls, the event loop runs every queued microtask, those queued along the way included, before anything else.

Rendering waits its turn too. When a frame is due, roughly every 16.7 ms on a 60 Hz screen, the browser runs the animation-frame callbacks, recalculates style and layout, then paints. A task that holds the stack for 200 ms leaves no room for any of it: the page cannot update or run a click's handler until the task is over.

Timers count in parallel, off the main thread. setTimeout(cb, 0) only asks for cb to be queued as a task once at least 0 ms have passed; then it waits for the event loop to pick it. That is why the script in this machine logs A, D, C, then B.

The run, step by step

Every scenario, in words.

What the machine plays, step by step. Beside a step, what a what-if switch plays in its place, or adds right after it.

Run a script

8 steps
  1. Play

    The script runs as one task

    The script logs A, sets a 0 ms timer for B, queues C on a resolved promise and logs D. All of it is one task: the event loop takes it from the task queue and runs it on the call stack.

  2. Play

    A runs at once

    console.log("A") is plain synchronous code: it runs right away, on the stack.

    • With Block the stack for 200 ms · adds a step after it

      Play

      A 200 ms loop holds the stack

      A loop spins for 200 ms. One thread, one stack: while it runs, nothing else can, not a timer's callback, not a click, not a frame.

  3. Play

    setTimeout hands B to a timer

    setTimeout(cb, 0) does not run cb. It gives it to a timer, which counts in parallel, off the main thread. 0 ms means at least 0 ms.

  4. Play

    The promise queues C as a microtask

    Promise.resolve().then(cb) queues cb straight away, as a microtask. It will run as soon as the stack is empty, before the next task.

  5. Play

    D, and the script is done

    The last line logs D and the script's task ends: the stack is empty. Once its 0 ms are up, the timer queues B in the task queue, where B waits its turn.

  6. Play

    Microtasks first: C

    The stack is empty, so the event loop runs a microtask checkpoint: it empties the microtask queue, C included, before it takes another task.

    • With Chain microtasks forever

      Play

      C queues C again

      C logs, then queues itself as a new microtask. The checkpoint only ends when the microtask queue is empty, and now it never is.

    • With Block the stack for 200 ms · adds a step after it

      Play

      A frame, 200 ms late

      A frame has been due since the loop began, so it most likely comes before B. About 12 frames were never drawn at 60 Hz, and each click waited for the stack, then ran late.

  7. Play

    Then the next task: B

    Only now can the event loop take another task: here, the timer's callback, which logs B. The output reads A, D, C, B.

    • With Chain microtasks forever

      Play

      The page freezes

      B never runs and no frame is drawn: the event loop never leaves the checkpoint. Clicks pile up in the task queue, unanswered.

    • With Chain timeouts forever

      Play

      B runs, and sets B again

      Here the next task is the timer's callback: it logs B, then calls setTimeout(B, 0) again. The next B will be a new task, queued once its timer is up, so frames and other tasks can go first.

  8. Play

    A frame, when one is due

    Between two tasks the browser may render: animation-frame callbacks, then style, layout and paint. At 60 Hz one is due within 16.7 ms, so it can also come before B.

    • With Chain timeouts forever · adds a step after it

      Play

      The page stays responsive

      Each B is its own task, so frames and clicks fit in between. Once timers nest more than five deep, browsers wait at least 4 ms between rounds.