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.
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.
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
Move the timer to the bottom
In the first example, move the
setTimeoutline to the very end of the script. Predict whether the order changes, then run it. - 2
Chain a second then
In the second example, add
.then(() => console.log("then 2"))after the first.then(...). Predict wherethen 2lands: before or aftertimer 1? - 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
The timer was set first, yet it runs last: microtasks always run before the next task.
Preview
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)The microtask created inside timer 1 runs before timer 2, because the microtask queue is emptied after every task.
Preview
Console
Nothing logged yet.Expected console output
sync
microtask 1
microtask added by microtask 1
timer 1
microtask from timer 1
timer 2With `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
Console
Nothing logged yet.Expected console output
listener 1
listener 2
after btn.click()
microtask 1
microtask 2The 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
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.
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.
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.nextTickcallbacks before promise microtasks, and its loop has phases (timers, I/O poll,setImmediatecheck). In Node, the order ofsetTimeout(f, 0)andsetImmediate(g)in the main script is not guaranteed, though inside an I/O callbacksetImmediatealways runs first. - ▸
requestAnimationFramecallbacks run in the rendering step, just before style and layout; they're the right place to read and write layout for animations.requestIdleCallbackruns when the browser has spare time in a frame. - ▸
An
awaitcosts 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 madeawaiton a native promise take fewer ticks, which is why some old blog puzzles now give different answers.
Remember this
- 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
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
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 afterawait) are microtasks, and so is anything you queue withqueueMicrotask(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
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
requestAnimationFramecallbacks, 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
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 beforesetTimeout(b, 0), no matter which line came first. - 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
whileloop. 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
Explain the event loop: call stack, task queue, microtask queue and rendering.
What is the difference between a task and a microtask? Name examples of each.
Predict and explain: setTimeout(() => log(1)); Promise.resolve().then(() => log(2)); log(3);.
How can microtasks freeze a page, and why can't the user see DOM changes during a long task?
Practice
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");
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.