Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate ~21 min· topic 3 of 8

Topic 10.3

The Event Loop

In one line

The event loop is the rule that decides what runs next: finish the current code until the call stack is empty, run every microtask (promise callbacks), let the browser paint if it's time, then take the next task (a timer, a click) and repeat.

Think of it like this

A doctor's clinic with one doctor. The doctor sees one patient at a time and never stops halfway (that's the call stack). Patients who arrive wait in the waiting room (the task queue). But after every patient, the doctor first handles all the quick follow-ups from the nurse ("sign this prescription") before calling anyone new: that's the microtask queue. Every so often, the clinic is tidied for visitors: that's rendering the page.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Call stack
The engine's list of functions running right now; the top one is the one executing.
Run to completion
Once a function starts, it finishes before any other JavaScript runs.
Task (macrotask)
A whole job queued by the browser: the page script, a timer callback, a click handler.
Task queue
The line of tasks waiting for the call stack to be empty.
Microtask
A small job (promise callbacks, queueMicrotask) run right after the current code, before the next task.
Microtask queue
The line of microtasks, emptied completely after every task.
Event loop
The browser's endless cycle: run a task, run all microtasks, maybe paint, repeat.
Rendering
The browser updating styles and layout and painting pixels so changes show on screen.

Step by step

01The page script is the first task

The browser runs your script as a task. Each line runs in order. When code calls setTimeout, the browser starts a timer outside JavaScript. When code calls Promise.resolve().then(fn), fn goes into the microtask queue. Neither runs yet: the stack is still busy with your script.

order.jswhole filejavascript
console.log("A");
setTimeout(() => console.log("D"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("B");

02The stack empties: microtasks run

The script ends and the stack is empty. Before anything else, the event loop runs every microtask: C prints. Meanwhile the 0 ms timer has fired, so its callback is waiting in the task queue.

03The next task runs

With no microtasks left, the browser may paint, then takes the next task: the timer callback, which prints D. Final order: A, B, C, D.

The next task runsdiagram
Rendering diagram…

04Microtasks added by microtasks run too

If a microtask queues another microtask, the new one runs in the same round, still before any timer. That's why a promise chain of ten .then steps all finishes before a 0 ms timer fires.

05Rendering waits for your code

If a click handler sets a spinner's hidden = false, then does two seconds of work, then hides it again, the user never sees the spinner: painting only happens between tasks. To show it, let the browser paint first (for example, start the work in a setTimeout or after requestAnimationFrame).

06Predict, step by step

For any puzzle, write three columns: sync, microtasks, tasks. Go through the code once, filling the columns. Print the sync column, then the microtasks in order (adding any new ones to the end), then each task in order, each followed by the microtasks it creates.

Try it yourself

  1. 1

    Move the timer to the bottom

    In the first example, move the setTimeout line to the very end of the script. Predict whether the order changes, then run it.

  2. 2

    Chain a second then

    In the second example, add .then(() => console.log("then 2")) after the first .then(...). Predict where then 2 lands: before or after timer 1?

  3. 3

    Click it yourself

    In the third example, delete the btn.click(); line and the log after it, run, and click the button. Compare the order with the scripted click.

Code & diagrams

Sync first, then microtasks, then tasks New tab

The timer was set first, yet it runs last: microtasks always run before the next task.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

1. script start
2. script end
3. promise .then (a microtask)
4. queueMicrotask (a microtask)
5. setTimeout (a task)
Each task gets its own microtasks New tab

The microtask created inside timer 1 runs before timer 2, because the microtask queue is emptied after every task.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

sync
microtask 1
microtask added by microtask 1
timer 1
microtask from timer 1
timer 2
click() from code vs a real click New tab

With `btn.click()` the stack is never empty between listeners (our script is still running), so microtasks wait until the end. Now click the button yourself: you get `listener 1`, `microtask 1`, `listener 2`, `microtask 2`, because the browser calls each listener from an empty stack.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

listener 1
listener 2
after btn.click()
microtask 1
microtask 2
Rendering happens between tasks, not during them New tab

The first button freezes for over a second and you only ever see Step 3. The second shows each step, because the browser can paint between tasks.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Starve the event loop with microtasks

Write function again() { queueMicrotask(again); } again(); in a page with a button.

terminal
$ the page shows
── what you'll see ──
The page freezes completely: no clicks, no repainting, timers never fire. Eventually the browser offers to stop the page.

Break #2

Expect a spinner during synchronous work

In a click handler, set spinner.hidden = false, run a 2-second calculation, then set spinner.hidden = true.

terminal
$ the page shows
── what you'll see ──
The spinner never appears; the page just freezes for 2 seconds.

Myth vs fact

Myth

setTimeout(fn, 0) and Promise.resolve().then(fn) are the same "run later".

Fact

The promise callback is a microtask and runs before the next task; the timer is a task and runs after all microtasks (and possibly after a paint).

Myth

The browser repaints after every DOM change.

Fact

It paints at most once per frame, between tasks. Several changes in one task appear together, and only their final state is drawn.

Myth

The event loop is part of the JavaScript language.

Fact

ECMAScript defines jobs (microtasks), but the event loop with its task queues and rendering steps is defined by the host: the HTML standard in browsers, libuv in Node.js.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Browsers have several task queues (timers, user input, networking) and may pick between them by priority; input is often favoured. The spec only guarantees order within one queue, so don't rely on the order between a timer and a network callback.

  • ▸

    Node.js runs process.nextTick callbacks before promise microtasks, and its loop has phases (timers, I/O poll, setImmediate check). In Node, the order of setTimeout(f, 0) and setImmediate(g) in the main script is not guaranteed, though inside an I/O callback setImmediate always runs first.

  • ▸

    requestAnimationFrame callbacks run in the rendering step, just before style and layout; they're the right place to read and write layout for animations. requestIdleCallback runs when the browser has spare time in a frame.

  • ▸

    An await costs at least one microtask turn even on a value that isn't a promise, so code after it runs after the current synchronous code. A 2019 change to the spec made await on a native promise take fewer ticks, which is why some old blog puzzles now give different answers.

Remember this

  1. 1

    The call stack is the engine's list of functions that are running right now. Calling a function pushes it on top; returning pops it off (you met it in Topic 3.8). JavaScript only runs what's on the stack, and it runs it to completion: nothing else can interrupt a running function. When the stack is empty, the engine is idle and asks the event loop what to do next.

  2. 2

    A task (also called a macrotask) is a whole piece of work the browser queues: running your page's <script>, a timer callback, an event listener for a click, a network response handler. Tasks wait in task queues. The event loop takes one task, runs it until the stack is empty, and only then considers the next.

  3. 3

    A microtask is a small job that must run as soon as the current code finishes, before any other task. Promise callbacks (.then, .catch, .finally, and the code after await) are microtasks, and so is anything you queue with queueMicrotask(fn). After each task, the loop runs every microtask in the microtask queue, including microtasks added while doing so, until the queue is empty.

  4. 4

    One turn of the event loop: (1) take the oldest task and run it; (2) run all microtasks; (3) if it's time for a new frame (usually 60 times a second), render: run requestAnimationFrame callbacks, recalculate styles and layout, and paint; (4) repeat. So DOM changes you make in a task don't appear on screen until the task and its microtasks finish. Changing text ten times in one loop paints only the last value.

  5. 5

    That gives you the rule for predicting output order: first all synchronous code, then all microtasks (in the order they were queued), then tasks such as timers (in the order they become ready), each followed by its own microtasks. So Promise.resolve().then(a) always runs before setTimeout(b, 0), no matter which line came first.

  6. 6

    The flip side: microtasks can starve the page. If every microtask queues another one, the queue never empties, so the loop never reaches rendering or the next task, and the page freezes just as if you'd written an endless while loop. Long tasks block in the same way. The event loop belongs to the host: the browser's comes from the HTML standard and Node.js has its own (built on a library called libuv), so a few details differ.

Explain it without notes

01

Explain the event loop: call stack, task queue, microtask queue and rendering.

02

What is the difference between a task and a microtask? Name examples of each.

03

Predict and explain: setTimeout(() => log(1)); Promise.resolve().then(() => log(2)); log(3);.

04

How can microtasks freeze a page, and why can't the user see DOM changes during a long task?

Practice

01

Predict the output, then check it in a live example: console.log("a"); setTimeout(() => console.log("b"), 0); Promise.resolve().then(() => console.log("c")).then(() => console.log("d")); console.log("e");

02

Write code that prints sync, micro, task in that order, using one of each mechanism, with the lines written in the order task, micro, sync.

Trade-offs

  • ↔

    A microtask runs sooner (before paint and input) and keeps related steps together, but too many delay rendering; a task (setTimeout) lets the browser paint and respond in between, at the cost of running later and being clamped or throttled.

  • ↔

    One thread with an event loop means no locks and no data races between your callbacks, but any long piece of work blocks everything; the trade is simplicity for the duty to keep tasks short.

Done when you can

  • I can draw the event loop: stack, task queue, microtask queue and rendering.

  • I can name examples of tasks and of microtasks.

  • I can predict the output order of mixed sync, promise and timer code.

  • I know DOM changes are painted only between tasks.

  • I can explain how endless microtasks freeze a page.