cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

The HTML event loop processing model

Learn the HTML event loop's task choice, microtask checkpoint, rendering opportunity, pausing rules, agents, workers, and host hooks.

By the end, you can
  • 01
    Trace a browser turnName the chosen task, the full microtask checkpoint, and the later rendering opportunity.
  • 02
    Separate host conceptsDistinguish task sources, agents, window loops, worker loops, and the promise host hooks.
  • 03
    Diagnose orderingPredict stable script, promise, and timer output without guessing a frame time.

The browser's fixed round

An event loop is the host routine that lets a browser run script, callbacks, promise jobs, and rendering without running two JavaScript callbacks at once on the same loop. It has named steps; “async” does not mean random.

Real-life analogyOne cashier's round
Think of one cashier with several lines: clicks, timers, and network replies. Each round, the cashier serves one customer from a chosen line, serves everybody in the express lane, then updates the price board when it is time. An alert() is the cashier stopping everything until you press OK.
In real life: A customer from one line
In JavaScript: One task from a task source
In real life: Everyone in the express lane
In JavaScript: The microtask checkpoint
In real life: Update the price board
In JavaScript: Update rendering when appropriate
In real life: A stopped cashier at OK
In JavaScript: A paused alert dialog

Where the analogy stops: A browser has richer rules and can use several task queues; the analogy only shows the round's shape.

Script, promise, then timerPop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("script");

Line 1 makes a later timer task. Line 2 asks the host to enqueue a promise reaction as a microtask. Line 3 runs immediately, so the real output is script, then promise, then timer.

Definition

One normal turn chooses one task, performs a microtask checkpoint until empty, and may update rendering for a window. The exact task choice is host-controlled.

The useful question is therefore not “which callback is asynchronous?” It is “what work is already running, what queue receives the later work, and where is the next checkpoint?” That question works for a promise, a click, a timer, and a message from a worker.

Event loops, agents, and windows

An agent is a group with its own JavaScript execution context and event loop. A browser window belongs to an agent; similar-origin windows can share a window agent, so they can share a loop.

One task finishes before another startsPop out in the code editor (opens in a new tab)JavaScript
console.log("start task");
console.log("finish task");

Line 1 starts this task. Line 2 finishes it before this loop picks another task. That rule makes ordinary DOM code easier to reason about.

A worker is not a second callback inside the window loop. It has its own agent and its own event loop. Messages cross between them as later tasks.

An agent is a specification idea, not a promise about one operating system thread. A browser may schedule loops in different ways. The stable fact for application code is that callbacks on one event loop do not interleave halfway through ordinary synchronous JavaScript.

A window also has a realm and a global object. Those names matter when the specification says which Window owns a timer, a promise, or a DOM object. They do not mean that every same-origin tab necessarily shares one loop; browsing-context grouping matters.

Task queues and task sources

A task source groups work such as user input, timers, or networking. The specification keeps sources separate so the host can make sensible choices rather than pretending every callback sits in one FIFO list.

Two ways to queue later workPop out in the code editor (opens in a new tab)JavaScript
console.log("script");
queueMicrotask(() => console.log("microtask"));
setTimeout(() => console.log("timer"), 0);

Line 1 logs during the current script task. Line 2 queues a microtask, while line 3 queues a timer task. The output is script, microtask, timer.

Do not infer a cross-source priority rule from one run. A ready timer and a click are different sources; the browser chooses according to its rules and current state.

The HTML specification deliberately calls task queues sets, because it selects the first runnable task from a chosen queue. It also lets a browser associate several sources with one queue. That flexibility protects responsiveness while preserving the order of related work from the same source.

A task source answers “what kind of work created this?” It does not tell application code a universal priority. User interaction, networking, DOM work, navigation, and rendering all need different scheduling decisions from the host.

Choosing the next task

The loop chooses a runnable task, not every task. This small model makes the choice visible: change only its rule and compare the first task while the same microtask checkpoint still follows.

Playground: choose a task rule
One modeled loop turnJavaScript
const queues = {
  input: ["click"],
  timer: ["refresh"],
  network: ["reply"],
};

const task = pickNextTask(tasks, rule);
runTask(task, microtasks);
runMicrotaskCheckpoint(microtasks);
maybeRender(now);
Modeled result

Chosen task: refresh

Microtasks: promise cleanup

Rendering: render

Try it yourself
Choose the teaching-model rule

The model chose refresh. It then drained promise cleanup and would render.

This is a teaching model. HTML has task sources and selection rules; it does not expose one universal author-controlled priority switch.

The “input first” option is a teaching choice, not a button browsers expose. Its purpose is to show why task sources exist and why application code must not rely on a universal timer-versus-click ordering.

Under “oldest task first”, the modeled refresh was queued first and wins. Under “user input first”, the click wins even though it arrived later. In both cases the selected task finishes before the model drains its microtasks. That unchanged part is the key distinction.

Real browsers choose a queue in an implementation-defined way, then take its first runnable task. Write code that is correct whichever eligible source wins. When order matters, create an explicit promise or send an explicit message instead of depending on timing luck.

Performing a microtask checkpoint

A microtask checkpoint drains the microtask queue until it is empty. Promise reactions use this checkpoint, which is why they run before the event loop chooses a later timer task.

A checkpoint can grow while drainingPop out in the code editor (opens in a new tab)JavaScript
const microtasks = ["first promise"];

function runMicrotaskCheckpoint(queue) {
  const output = [];
  while (queue.length) {
    const job = queue.shift();
    output.push(job);
    if (job === "first promise") queue.push("nested promise");
  }
  return output;
}

console.log(runMicrotaskCheckpoint(microtasks).join(","));

Line 1 starts with one job. Line 7 removes a job; line 9 adds nested work when the first promise runs. The loop checks again, so line 12 prints first promise,nested promise.

Step through a growing microtask checkpoint
Step 0 of 5Ready
Your turn: follow the blue line

Replay of a real lesson function. It models the HTML microtask checkpoint's drain-until-empty rule.

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
 function runMicrotaskCheckpoint(queue) {  const output = [];  while (queue.length) {    const job = queue.shift();    output.push(job);    if (job === "first promise") queue.push("nested promise");  }  return output;} console.log(runMicrotaskCheckpoint(microtasks).join(","));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
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.

Keep microtasks short. A chain that keeps enqueueing more microtasks delays tasks, input, and any rendering opportunity.

Notice that “until empty” includes work added by the work that is already draining. This is why a promise callback can queue another promise callback and both complete before a ready timer. It is not a special promise rule; it follows from the checkpoint loop.

The platform also performs bookkeeping after the queue is empty, including rejected-promise notification and clearing kept objects. Those details are not a reason to use microtasks as a general job queue. A long chain can make a page feel frozen without blocking JavaScript in the traditional sense.

Update the rendering

For a window loop, the host may update rendering after the checkpoint. This is an opportunity, not a promise that every task produces a frame or that a particular timer lands on a particular frame.

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

Replay of an instrumented teaching model, not a browser engine debugger.

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
  { source: "timer", label: "timer", queuedAt: 0 },  { source: "input", label: "click", queuedAt: 1, microtasks: ["save cart"] },];const microtasks = ["promise cleanup"]; const task = pickNextTask(tasks, "oldest");runTask(task, microtasks);runMicrotaskCheckpoint(microtasks);const rendering = maybeRender(16);console.log(task.label, rendering);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
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.

The replay uses simulated time only to make a visible model. It shows task, checkpoint, then “consider rendering”; it does not measure a browser frame.

What a rendering update can include
StepWhy it is thereWhat authors should conclude
Choose renderable documentsHidden, blocked, or ineligible documents can be skipped.A DOM change does not guarantee visible pixels yet.
Run window update stepsResize, scroll, media queries, and animations are coordinated.Do not assume each callback creates a standalone frame.
Run animation-frame callbacksFrame callbacks see a rendering opportunity before style and layout.Use the next lesson for exact animation-frame timing.
Recalculate style and layoutThe document works out what should be drawn and where.Keep a task short so this work gets a chance to happen.
Update visible UIPainting and presentation reflect the current document state.Treat this as conditional host work, not a callback guarantee.

The HTML processing model filters documents that are hidden, render-blocked, or have no rendering opportunity. It can also skip an update when there is nothing visible to change. That is why a DOM write means “the document state changed”, not “new pixels are already on screen”.

When a window does update, the specification includes resize and scroll steps, media-query changes, animation updates, animation-frame callbacks, style and layout, observers, paint timing, and updating the visible user interface. The next lesson studies that path in detail; this lesson only fixes its place in the loop.

The next lesson, Rendering & the event loop, follows this step into animation frames, style, layout, and paint.

Window and worker event loops

Window loops can have a rendering update because they own a document and pixels. A dedicated worker has its own event loop, can receive messages and run promise jobs, but has no window rendering step.

A worker receives a later message taskJavaScript
// worker.js
self.onmessage = (event) => {
  console.log("worker received", event.data);
};

Line 2 runs when the worker loop receives a message task. The worker can answer with postMessage, and the window handles that answer as another later task.

This separation avoids sharing mutable JavaScript state by default. Use messages for data, and reserve shared memory for the specific cases that need it.

The worker does still have tasks and a microtask queue. A promise reaction in the worker follows its worker loop, while a message sent back to the window becomes later work for the window loop. Neither side gets to interrupt the other side's running callback.

A dedicated worker can support its own rendering in special OffscreenCanvas cases, but it does not update the page's window document. Keep the basic rule: page rendering belongs to the window loop that owns the document.

Spinning and pausing

Some platform operations pause the loop. While a modal alert() is open, ordinary tasks and microtasks do not continue around it. Synchronous XMLHttpRequest has a similar blocking problem and should be avoided in responsive page code.

A safe sandbox dialog demonstrationPop out in the code editor (opens in a new tab)JavaScript
console.log("before dialog");
alert("The event loop is paused until this closes.");
console.log("after dialog");

Line 1 prints before the dialog. Line 2 pauses until it is dismissed; the lesson sandbox may dismiss it automatically. Then line 3 prints after dialog. Nothing scheduled in between gets to run meanwhile.

The specification also has “spinning the event loop” algorithms for special nested waiting cases. Treat them as platform machinery, not a pattern for application control flow.

Pausing and spinning are not the same. A paused task waits: the corresponding loop does not run further tasks, and script in the current task blocks. Spinning saves the current stack, performs a checkpoint, and lets selected later work drive a special algorithm forward before the old task resumes.

Both ideas are historical tools for platform algorithms. In page code, use asynchronous APIs and return to the loop. A synchronous request or a blocking dialog makes the cashier in the analogy stop serving every normal line.

Host hooks for promise jobs

ECMAScript defines promises, but it asks the host where to schedule their reactions. In browsers, HostEnqueuePromiseJob hands a reaction to the HTML microtask queue.

A promise reaction reaches the host queuePop out in the code editor (opens in a new tab)JavaScript
Promise.resolve("order").then((value) => {
  console.log("promise job", value);
});
console.log("script");

Line 1 registers a reaction. Line 2 logs the fulfilled value when the checkpoint runs. Line 4 logs now, so the output is script followed by promise job order.

HostPromiseRejectionTracker lets the host observe unhandled rejections. HostEnqueueTimeoutJob is another host hook, used by Atomics.waitAsync to enqueue its timeout work. See Promise internals for the promise side.

Other hooks preserve context when the host calls a job callback and can enqueue generic or finalization cleanup jobs. They are not APIs that application code calls. They explain how the ECMAScript promise model reaches a browser's queues without baking browser details into the language itself.

For the matching language-level model, read Microtask queue internals. It shows the queue from JavaScript's point of view before this lesson returns to the browser host that runs it.

Practical browser code

Use this model to explain an interface, not to force an ordering the platform never promises. A click handler can synchronously change state, queue a small promise reaction for consistent follow-up work, and let rendering happen later.

Terms that are easy to mix up
TermMeaningExample
TaskOne selected runnable unit from a task sourceA timer callback or a click handler
MicrotaskJob drained at the checkpoint before the loop continuesA promise reaction or queueMicrotask callback
Rendering opportunityA window-loop chance to update pixels after workStyle, layout, paint, and compositing when needed
Paused loopThe current loop waits and does not run other workA modal dialog or synchronous XHR
Sort the browser work
  • A click handler from user input
  • A ready setTimeout callback
  • A fulfilled promise reaction
  • A queueMicrotask callback
  • A possible update of window rendering
  • A modal dialog waiting for dismissal
Try it yourself
0 of 6 correct

Place each item by where it belongs in the loop model.

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

For everyday grounding, revisit the event loop, microtasks, timers, and the main thread.

Common misconceptions

  • “All async callbacks use one queue.” Task sources are distinct, and microtasks are separate.
  • “A timer of zero runs immediately.” It becomes eligible for a later task turn.
  • “The browser paints after every callback.” Rendering is a conditional opportunity.
  • “Workers render in the background.” Workers run code; window loops render documents.
  • “Modal waiting lets promises continue.” Pausing blocks ordinary progress until the operation ends.

Practice exercises

Exercise 1 · Warm-upPredict script, promise, timer

Type the comma-separated output.

Starter codePop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("script");

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

    Exercise 2 · Warm-upDrain a growing checkpoint

    Type the output.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const microtasks = ["first promise"];
    
    function runMicrotaskCheckpoint(queue) {
      const output = [];
      while (queue.length) {
        const job = queue.shift();
        output.push(job);
        if (job === "first promise") queue.push("nested promise");
      }
      return output;
    }
    
    console.log(runMicrotaskCheckpoint(microtasks).join(","));

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

      Exercise 3 · PracticePick a task

      Which task is selected by the model's input-first rule?

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

        Exercise 4 · PracticeName the worker difference

        Does a worker loop update window rendering?

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

          Exercise 5 · ChallengeExplain a dialog pause

          What happens to other queued work while alert() is open?

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

            Exercise 6 · ChallengeApply it to a cart

            After a cart click updates state, what kind of queue suits a tiny consistent follow-up?

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

              Check your understanding

              Predict output from the queues, then separate what the standard guarantees from what a browser may choose.

              HTML event loop quiz · 6 questionsScore: first tries count
              1. Question 1 of 6What does the first snippet print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                setTimeout(() => console.log("timer"), 0);
                Promise.resolve().then(() => console.log("promise"));
                console.log("script");

                Choose an answer to see the explanation.

              2. Question 2 of 6How many normal tasks does one loop turn choose in this model?

                Choose an answer to see the explanation.

              3. Question 3 of 6What must a microtask checkpoint do after a running job queues another microtask?

                Choose an answer to see the explanation.

              4. Question 4 of 6Which loop can include a rendering update step?

                Choose an answer to see the explanation.

              5. Question 5 of 6What does HostEnqueuePromiseJob provide to JavaScript promises?

                Choose an answer to see the explanation.

              6. Question 6 of 6What is safe to say about rendering after a task?

                Choose an answer to see the explanation.

              Key takeaways

              • One turn chooses one task, then drains microtasks until empty.
              • Task sources and microtasks are different scheduling concepts.
              • Window loops may render after the checkpoint; workers do not render windows.
              • Dialogs and synchronous operations pause normal progress.
              • Promise jobs reach HTML through host hooks.

              Remember the one-liner.
              A browser turn is one chosen task, every queued microtask, then a possible rendering update.

              Coming next: Rendering & the event loop.

              CompleteFrontend Clear concepts. Working examples.