cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

The event loop

Understand the JavaScript event loop: the call stack, task queue, microtask queue, rendering steps, and how starvation freezes the page.

By the end, you can predict
  • 01
    Which line runs nextTrace synchronous code, timer tasks, and microtasks in the right order.
  • 02
    When rendering can happenExplain why many style changes inside one task produce only one painted result.
  • 03
    Why 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.

Real-life analogyOne cashier, two queues

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 appear here, but only barely

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 THROUGH

The 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.

Step through one event-loop turn
Step 0 of 6Ready
Your turn: follow the blue line

Predict the final console order. Then step through a model recorded from real JavaScript scheduling rules.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("task"), 0);queueMicrotask(() => console.log("microtask"));console.log("script end");
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the microtask API shown on line 3

Both choices queue a microtask here, so the final order stays the same.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Real-life analogyA meeting with one talking stick

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

INTERACTIVE

Microtasks 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.

Event-loop visualizer: model plus real browser run
Timer plus two microtasksPop out in the code editor (opens in a new tab)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");
Model stateScript begins
Call stackscript
Microtask queue—
Task queue—
Console—

Final order to predict: script start → script end → promise microtask → queued microtask → timeout task

Real run: —

Step 1 of 9Script begins

A task starts by running the script on the call stack. Model console: —. Real browser console: —.

The columns are a teaching model, not an engine debugger. The Run for real button executes the same preset in your browser and captures the actual log order.
Event-loop queues and steps
Queue or stepWhat goes thereWhen it runsMental model
Call stackThe function currently runningRight now, until it returnsThe ride car
Task queueScript start, timers, user events, message eventsOne task per event-loop turnThe main line
Microtask queuePromise reactions, queueMicrotask, MutationObserverAfter a task, drained completelyThe fast-pass line
Rendering opportunityStyle, layout, paint, plus requestAnimationFrame just before paintBetween tasks, after microtasksThe 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.

Task or microtask?
  • setTimeout callback
  • click handler
  • Promise.resolve().then callback
  • queueMicrotask callback
  • MutationObserver callback
  • requestAnimationFrame callback
Try it yourself
0 of 6 correct

Sort each callback by where it belongs. requestAnimationFrame is included because it is often confused with both queues.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Rendering steps

REAL BROWSER

JavaScript 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.

Rendering: one task versus several turns
Two ways to change a boxPop out in the code editor (opens in a new tab)JavaScript
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);  });});
Paint targetwatch the box
  1. Ready: click a run button.
Try it yourself

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.

This is a real browser demo. Reduced-motion users get shortened waits while preserving the ordering.
Why only the last synchronous color appears

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 DEMO

Starvation 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.

Starvation: a bounded fast-pass line
Bounded starvation patternPop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("timer"), 0);let count = 0;function spinMicrotasks(limit) {  count = count + 1;  if (count < limit) {    queueMicrotask(() => spinMicrotasks(limit));  }}queueMicrotask(() => spinMicrotasks(50000));
Safer chunk shapeJavaScript
for (let i = 0; i < items.length; i += 1) {  doSmallPiece(items[i]);  if (i % 100 === 99) {    setTimeout(processNextChunk, 0);    return;  }}
Measured delaymicrotasks

Choose a mode, then run the bounded demo.

Try it yourself

The demo always stops. The microtask version delays the already-pending timer until the chain ends; the task-chunked version yields between chunks.

Use modest iteration counts on slower devices. This intentionally demonstrates delay without creating an infinite loop.
The professional rule of thumb

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?

Practical event-loop choices
SituationGood choiceWhy
Show a spinner before expensive workStart the work in a later taskThe browser needs a rendering opportunity to paint the spinner first.
Run tiny cleanup after the current function returnsqueueMicrotask(cleanup)It runs before the next task without waiting for a timer.
Prepare the next animation framerequestAnimationFrame(draw)The callback runs before paint, aligned with the browser’s frame.
Process thousands of items without freezingChunk work with tasksEach chunk yields so input and rendering can run.
A UI bug in one sentence

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.

Misleading but commonPop out in the code editor (opens in a new tab)JavaScript
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 EXERCISES
Exercise 1 · Warm-upPredict the order

Write the console output order with spaces or commas.

Starter codePop out in the code editor (opens in a new tab)JavaScript
console.log("A");
setTimeout(() => console.log("B"), 0);
queueMicrotask(() => console.log("C"));
console.log("D");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFix the spinner that never appears

    The spinner state changes, but the user never sees it before the loop. Which queue should the heavy work yield to: task or microtask?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    showSpinner();
    for (let i = 0; i < 100000000; i += 1) {
      crunch(i);
    }
    hideSpinner();

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeExplain starvation

      In one or two sentences, explain why an endless microtask chain can freeze clicks and painting.

        Exercise 4 · ChallengeReorder code to hit a target order

        Arrange a synchronous log, a microtask, and a timer so the output is exactly first second third.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        // Target: first, second, third
        setTimeout(() => console.log("third"), 0);
        console.log("first");
        queueMicrotask(() => console.log("second"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose queueMicrotask or setTimeout

          You need to run tiny bookkeeping after the current function returns, and it must happen before a pending timer callback. Which API fits?

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            7 QUESTIONS

            Predict before you click. Each wrong answer explains the exact queue rule it missed.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What runs while JavaScript is already executing a function?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log("one");
              setTimeout(() => console.log("two"), 0);
              Promise.resolve().then(() => console.log("three"));
              console.log("four");

              Choose an answer to see the explanation.

            3. Question 3 of 7Which callback is a microtask?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does this nested microtask snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              setTimeout(() => 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.

            5. Question 5 of 7When can the browser paint a style change?

              Choose an answer to see the explanation.

            6. Question 6 of 7What is microtask starvation?

              Choose an answer to see the explanation.

            7. 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; requestAnimationFrame runs 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.

            Rendering recapPop out in the code editor (opens in a new tab)JavaScript
            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.

            CompleteFrontend Clear concepts. Working examples.