The event loop
Understand the JavaScript event loop: the call stack, task queue, microtask queue, rendering steps, and how starvation freezes the page.
- 01Which line runs nextTrace synchronous code, timer tasks, and microtasks in the right order.
- 02When rendering can happenExplain why many style changes inside one task produce only one painted result.
- 03Why starvation freezes pagesSpot code that keeps feeding microtasks and choose a safer chunking strategy.
The event loop in one picture
JavaScript on a web page has one main thread for running your code. That does not mean everything must finish immediately. The browser can wait for timers, clicks, network replies, and rendering work while your JavaScript is not on the call stack. The event loop is the rulebook for choosing what runs next.
Here is the useful beginner definition: the event loop runs one task, lets all microtasks finish, gives the browser a chance to render, then starts the next task. Details vary between hosts, and Stage 11 goes into the HTML processing model. This lesson gives you the model you need to debug real output-order puzzles today.
One cashier serves one customer at a time. Between regular customers, the cashier clears the express queue, then the display can update before the next customer is served.
- In real life: One cashier serving a customer
- In JavaScript: The call stack: one running path of JavaScript
- In real life: The regular queue
- In JavaScript: The task queue: timers, clicks, and script starts
- In real life: The express queue
- In JavaScript: The microtask queue: promise reactions and
queueMicrotask - In real life: The display updates between customers
- In JavaScript: Rendering: style, layout, and paint between turns
Where the analogy stops: A cashier may choose a fairer order. The browser drains every microtask before starting the next task.
Promises are taught in Module 22. For now, you only need this: Promise.resolve().then(callback) is one common way to queue a microtask, and queueMicrotask(callback) is the direct way to queue one.
We will go deeper than the Runtime at a glance lesson without repeating it: first the call stack and task queue, then the microtask queue, then rendering, then starvation.
The call stack & task queue
STEP THROUGHThe call stack is the stack of function calls currently running. You met it in The call stack lesson: a function can call another function, and JavaScript returns back down the stack. The important event-loop rule is run to completion: once a task starts running JavaScript, another task cannot jump in halfway.
A task is a turn in the main line. Loading a script, running a timer callback, and handling a click are tasks. When you write setTimeout(fn, 0), fn is not placed on the stack immediately. It waits as a future task. That is why a zero-delay timer still waits for the current script.
Predict the final console order. Then step through a model recorded from real JavaScript scheduling rules.
script
setTimeout(() => console.log("task"), 0);queueMicrotask(() => console.log("microtask"));console.log("script end");In the replay, line 2 places a timer callback in the task queue. It is ready soon, but ready is not the same as running. Line 4 still prints before the timer because the current task has to finish first. Then microtasks get their special checkpoint before the next task.
A calm meeting has one talking stick. Whoever holds it gets to finish their sentence. Others can line up, but they do not interrupt the middle of a word. JavaScript tasks behave the same way.
- In real life: The person holding the stick
- In JavaScript: The current call stack
- In real life: People waiting to speak
- In JavaScript: The task queue
- In real life: No interruptions mid-sentence
- In JavaScript: Run to completion
Where the analogy stops: People can choose to pause mid-sentence. JavaScript cannot pause ordinary synchronous code unless it returns to the host.
The microtask queue
INTERACTIVEMicrotasks are a smaller, higher-priority queue checked at a microtask checkpoint. In browsers, a checkpoint happens after a task’s JavaScript finishes. The rule is stronger than “run one microtask”: the event loop keeps running microtasks until the microtask queue is empty.
That is why this common pattern surprises people: synchronous logs print first, promise or queueMicrotask callbacks print next, and timers print last. Try the visualizer. Step the model, then run the same preset for real in your browser. The model’s final console order is tested against real JavaScript.
console.log("script start");setTimeout(() => console.log("timeout task"), 0);Promise.resolve().then(() => console.log("promise microtask"));queueMicrotask(() => console.log("queued microtask"));console.log("script end");Final order to predict: script start → script end → promise microtask → queued microtask → timeout task
Real run: —
A task starts by running the script on the call stack. Model console: —. Real browser console: —.
| Queue or step | What goes there | When it runs | Mental model |
|---|---|---|---|
| Call stack | The function currently running | Right now, until it returns | The ride car |
| Task queue | Script start, timers, user events, message events | One task per event-loop turn | The main line |
| Microtask queue | Promise reactions, queueMicrotask, MutationObserver | After a task, drained completely | The fast-pass line |
| Rendering opportunity | Style, layout, paint, plus requestAnimationFrame just before paint | Between tasks, after microtasks | The park photographer |
A microtask can queue another microtask. That new callback joins the end of the same microtask drain, so it still runs before the next task. This is powerful for “finish tiny cleanup immediately after the current code” work, but dangerous if you keep adding more.
- setTimeout callback
- click handler
- Promise.resolve().then callback
- queueMicrotask callback
- MutationObserver callback
- requestAnimationFrame callback
Sort each callback by where it belongs. requestAnimationFrame is included because it is often confused with both queues.
Rendering steps
REAL BROWSERJavaScript and rendering share the browser’s main thread. When code changes an element’s style, the browser records that change. It still cannot update the pixels while your synchronous task is running. After the task ends and microtasks are empty, the browser may update style, calculate layout, and paint. This is a rendering opportunity, not a promise that every tiny change gets a separate frame.
requestAnimationFrame(callback) fits here: its callback runs before the browser paints a frame. It is not a task or a microtask. Use it when you want animation code to prepare the next paint. Use later tasks, such as setTimeout chunks, when you want to yield so input and painting can happen between chunks.
box.style.background = "red";box.style.background = "gold";box.style.background = "lime";// The browser can paint only after this task ends. requestAnimationFrame(() => { box.style.background = "red"; requestAnimationFrame(() => { box.style.background = "gold"; setTimeout(() => { box.style.background = "lime"; }, 0); });});- Ready: click a run button.
Inside one task, the browser records every style write but usually paints only after the task and its microtasks finish. requestAnimationFrame runs before a paint; setTimeout schedules a later task.
In the first run, all three assignments happen inside one task. By the time the browser can paint, the element’s current color is the last one. The earlier colors existed as JavaScript writes, but they never became separate frames on screen.
Starvation
SAFE DEMOStarvation means one kind of work keeps getting fed so other work does not get a turn. With the cashier analogy, the express queue never ends. The regular queue waits. The display waits. On a page, that can mean timers, clicks, and rendering all feel frozen.
The dangerous shape is a microtask that queues another microtask, over and over. The browser must keep draining microtasks before it can run the next task or paint. The demo below is deliberately bounded: it stops after the iteration count you choose, then reports how long an already-pending timer had to wait. Compare that with chunking work as later tasks.
setTimeout(() => console.log("timer"), 0);let count = 0;function spinMicrotasks(limit) { count = count + 1; if (count < limit) { queueMicrotask(() => spinMicrotasks(limit)); }}queueMicrotask(() => spinMicrotasks(50000));for (let i = 0; i < items.length; i += 1) { doSmallPiece(items[i]); if (i % 100 === 99) { setTimeout(processNextChunk, 0); return; }}Choose a mode, then run the bounded demo.
The demo always stops. The microtask version delays the already-pending timer until the chain ends; the task-chunked version yields between chunks.
Use microtasks for short, immediate follow-up work. Do not use them for long loops, animation, or work that should allow clicks and painting. Break heavy work into tasks, or in later lessons, consider tools such as workers for truly CPU-heavy jobs.
Where you’ll use this
Event-loop knowledge turns “JavaScript is weird” into a checklist. When an output order surprises you, ask: what is synchronous, what is a task, what is a microtask, and when can rendering happen?
| Situation | Good choice | Why |
|---|---|---|
| Show a spinner before expensive work | Start the work in a later task | The browser needs a rendering opportunity to paint the spinner first. |
| Run tiny cleanup after the current function returns | queueMicrotask(cleanup) | It runs before the next task without waiting for a timer. |
| Prepare the next animation frame | requestAnimationFrame(draw) | The callback runs before paint, aligned with the browser’s frame. |
| Process thousands of items without freezing | Chunk work with tasks | Each chunk yields so input and rendering can run. |
If you set loading = true and immediately start a huge synchronous loop, the spinner may never appear because rendering cannot happen until the loop ends. Yield first, then do the heavy work in chunks.
Common misconceptions
“setTimeout(fn, 0) runs immediately.”
It queues a later task. The current stack finishes, microtasks drain, rendering may happen, and only then can that timer task run.
“Promises make code run in parallel.”
A promise reaction is a microtask on the same main thread. It does not split your JavaScript across cores.
“One microtask runs after each task.”
The whole microtask queue drains after a task, including new microtasks queued by earlier microtasks.
“The browser paints every style assignment.”
Painting happens between turns. Several synchronous assignments usually collapse into the final visible state.
“requestAnimationFrame is just a faster setTimeout.”
It has a different place in the browser’s rendering cycle: before a paint, not in the ordinary task queue.
console.log("start");setTimeout(() => console.log("timer"), 0);Promise.resolve().then(() => console.log("promise"));console.log("end");The output is start, end, promise, timer. The timer is not late because the browser is slow; it is waiting in the task queue because the event-loop rules say it must.
Practice: predict and fix event-loop code
5 EXERCISESWrite the console output order with spaces or commas.
console.log("A");
setTimeout(() => console.log("B"), 0);
queueMicrotask(() => console.log("C"));
console.log("D");Line 1 prints A. Line 2 queues a task. Line 3 queues a microtask. Line 4 prints D. Then C drains as a microtask, and finally B runs as a timer task.
The spinner state changes, but the user never sees it before the loop. Which queue should the heavy work yield to: task or microtask?
showSpinner();
for (let i = 0; i < 100000000; i += 1) {
crunch(i);
}
hideSpinner();showSpinner();
setTimeout(() => {
processInChunks();
}, 0);Starting the heavy work in a later task gives the browser a chance to paint the spinner first. For very large work, split processInChunks into several later tasks too.
In one or two sentences, explain why an endless microtask chain can freeze clicks and painting.
Microtasks drain completely after a task. If each microtask queues another microtask, that fast-pass line keeps refilling, so the next task and the browser’s rendering step never get a turn.
Arrange a synchronous log, a microtask, and a timer so the output is exactly first second third.
// Target: first, second, third
setTimeout(() => console.log("third"), 0);
console.log("first");
queueMicrotask(() => console.log("second"));console.log("first");
queueMicrotask(() => console.log("second"));
setTimeout(() => console.log("third"), 0);first logs synchronously. The microtask logs second after the script ends. The timer task logs third last.
You need to run tiny bookkeeping after the current function returns, and it must happen before a pending timer callback. Which API fits?
Use queueMicrotask. It runs after the current stack clears but before the next task, including a timer task. Use setTimeout instead when you want to yield to input and rendering.
Quiz: check your understanding
7 QUESTIONSPredict before you click. Each wrong answer explains the exact queue rule it missed.
Question 1 of 7What runs while JavaScript is already executing a function?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconsole.log("one"); setTimeout(() => console.log("two"), 0); Promise.resolve().then(() => console.log("three")); console.log("four");Choose an answer to see the explanation.
Question 3 of 7Which callback is a microtask?
Choose an answer to see the explanation.
Question 4 of 7What does this nested microtask snippet print?
Read the code, then predictsetTimeout(() => console.log("task 1"), 0); queueMicrotask(() => { console.log("micro 1"); queueMicrotask(() => console.log("micro 2")); }); console.log("sync");Choose an answer to see the explanation.
Question 5 of 7When can the browser paint a style change?
Choose an answer to see the explanation.
Question 6 of 7What is microtask starvation?
Choose an answer to see the explanation.
Question 7 of 7You need to break a large UI update into chunks that let clicks and painting happen. Which is safer?
Choose an answer to see the explanation.
Key takeaways
- JavaScript runs one call stack at a time: the current task runs to completion.
- Timer callbacks and click handlers wait in the task queue.
- Promise reactions,
queueMicrotask, and MutationObserver callbacks are microtasks. - After every task, the microtask queue drains completely before the next task.
- Rendering can happen only between turns, after microtasks are empty;
requestAnimationFrameruns before paint. - Microtask chains can starve tasks and rendering, so chunk long UI work with tasks.
The event loop one-liner.
Run one task, drain all microtasks, render if the browser can, then take the next task.
box.style.background = "red";box.style.background = "gold";box.style.background = "lime";// The browser can paint only after this task ends. requestAnimationFrame(() => { box.style.background = "red"; requestAnimationFrame(() => { box.style.background = "gold"; setTimeout(() => { box.style.background = "lime"; }, 0); });});Up next: Microtasks in depth, where you will predict promise, queueMicrotask, and await ordering puzzles with much smaller clues.