The JS Event Loop Architecture: Complete Guide

The event loop is the mechanism that lets JavaScript handle async operations while staying single-threaded. Learn its architecture and how it coordinates the call stack with task queues.

7 min read

The event loop is the mechanism that lets JavaScript handle asynchronous operations while running on a single thread. It is not part of the JavaScript engine. It is provided by the host environment: the browser, Node.js, or Deno.

The engine runs code synchronously, one function at a time. The event loop decides what runs next. Together they create a concurrency model where async callbacks, user events, and rendering all take turns without ever interrupting each other.

Why JavaScript Needs an Event Loop

JavaScript was designed to run in a browser, responding to user clicks, network responses, and timers. A multi-threaded model would introduce race conditions for every DOM update. Instead, the language went single-threaded with an event-driven architecture.

The event loop is the scheduler. It picks the next piece of work from several queues and hands it to the engine.

While the engine runs that work, nothing else can run. When the engine finishes, the loop picks again. This is the run-to-completion model: every chunk of synchronous code runs fully before the next chunk begins.

The Architecture: Three Key Structures

The event loop architecture has three main parts working together.

Event loop architecture overview

The call stack runs synchronous code. The microtask queue holds Promise callbacks and other high-priority work.

The task queue holds setTimeout callbacks, event handlers, and network responses. Browser APIs like timers and fetch sit outside the loop and feed callbacks into the queues when their work completes.

The event loop follows a strict order. First, it drains every microtask. Then it takes exactly one task.

Then it drains every microtask again. This cycle repeats indefinitely.

The Event Loop Cycle Step by Step

Here is the exact sequence the event loop follows on every iteration.

  1. The call stack runs synchronous code until it is empty.

  2. The event loop checks the microtask queue and runs every callback in it, including any new microtasks added during processing.

  3. If a render frame is due, the browser paints updates to the screen.

  4. The event loop takes one callback from the task queue and pushes it onto the call stack.

  5. The call stack runs that callback to completion.

  6. Repeat from step 2.

This cycle runs thousands of times per second in a typical web page. Every click handler, every fetch response, and every animation frame flows through these same steps.

A code example shows the cycle in practice.

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

The synchronous calls run first on the call stack, printing A and D. The Promise handler is queued as a microtask.

The setTimeout callback is registered with the browser timer. When the stack empties, the event loop drains microtasks, printing C. Then it takes the task from the timer, printing B.

The output is A, D, C, B. For a full comparison of how these queues interact, see /javascript/call-stack-vs-task-queue-vs-microtask-queue-js.

Where the Event Loop Lives

The event loop is not inside the JavaScript engine. The engine provides the call stack, the heap, and the garbage collector. The host environment provides the event loop, the task queue, the microtask queue, and all Web APIs.

In a browser, the event loop is specified by the HTML standard. It coordinates JavaScript execution with DOM updates, CSS animations, and user input. In Node.js, the event loop is built on libuv and has additional phases for I/O callbacks and setImmediate.

This separation means the same V8 engine can power both a browser tab and a Node.js server, but each has its own event loop implementation suited to its environment.

Microtasks: The High-Priority Lane

Microtasks are callbacks that must run before the next task begins. They come from Promise.then, Promise.catch, Promise.finally, queueMicrotask, and MutationObserver.

The event loop drains the entire microtask queue after every task. If a microtask adds more microtasks, those also run before the next task. This means a runaway microtask chain can block the task queue and freeze rendering.

javascriptjavascript
function addMicrotasks(count) {
  if (count <= 0) return;
  queueMicrotask(() => {
    console.log(count);
    addMicrotasks(count - 1);
  });
}
 
addMicrotasks(5);

All five numbers print before any setTimeout callback runs, even with a delay of zero. The microtask queue drains completely. For a deeper comparison of microtasks and macrotasks, see /javascript/js-microtasks-vs-macrotasks-a-complete-guide.

Tasks: One at a Time

Tasks are callbacks from setTimeout, setInterval, event listeners, fetch response handlers, and I/O operations. The event loop takes one task per iteration, runs it to completion, then drains microtasks again before taking the next task.

This one-task-per-iteration design is why a long synchronous loop blocks all other tasks, including click handlers and rendering. The browser cannot process the next task until the current one finishes.

The event loop also handles rendering between tasks. If the browser determines a frame is due, it may run requestAnimationFrame callbacks and paint before taking the next task. This is why requestAnimationFrame is the right place to update animations: it runs just before the next paint.

The Event Loop in Node.js

Node.js extends the browser event loop with additional phases for server-side work. The libuv library provides the implementation with these phases in order: timers, pending callbacks, idle and prepare, poll, check, and close callbacks.

The timers phase runs setTimeout and setInterval callbacks. The poll phase handles I/O events. The check phase runs setImmediate callbacks.

Microtasks run after each phase, just like in the browser.

Node.js also has process.nextTick, which drains before the Promise microtask queue at each checkpoint. It is a Node-specific queue with the highest priority, but the Node.js docs now recommend queueMicrotask for new code, and using nextTick excessively can starve the event loop.

How Rendering Fits Into the Loop

In the browser, rendering is not a separate thread. It is coordinated by the event loop.

After draining microtasks, the browser checks if a render frame is needed. If so, it runs requestAnimationFrame callbacks, recalculates styles, updates the layout, and paints to the screen.

This means rendering cannot happen while synchronous code runs on the call stack. A long synchronous loop blocks all visual updates, making the page appear frozen. The event loop must complete its current task and drain microtasks before any paint can occur.

For smooth 60fps animations, each event loop iteration from task to paint must complete in under 16 milliseconds. If a task takes too long, frames drop and animations stutter.

Common Misconception: The Event Loop Is Not Multithreading

The event loop creates the illusion of parallelism, but JavaScript remains single-threaded. Only one piece of code runs at a time.

The event loop does not run code in the background. It schedules code to run later, when the call stack is empty.

True parallelism requires Web Workers, which run in separate threads with their own event loops. Workers communicate with the main thread through message passing, not shared variables.

Rune AI

Rune AI

Key Insights

  • The event loop is provided by the host environment, not the JavaScript engine.
  • It follows a fixed cycle: drain microtasks, take one task, repeat.
  • Microtasks (Promises) always run before the next task (setTimeout, events).
  • The call stack must be empty before the event loop can process any queued callback.
  • Each tab, iframe, and Web Worker has its own independent event loop.
RunePowered by Rune AI

Frequently Asked Questions

Is the event loop part of the JavaScript engine?

No. The event loop is provided by the host environment (browser or Node.js), not the JavaScript engine itself. The engine only executes the code the event loop hands it.

Can JavaScript have multiple event loops?

Yes. Each browsing context (tab, iframe) and each Web Worker has its own event loop. They run independently and communicate through message passing, not shared memory.

Conclusion

The event loop is what makes JavaScript feel asynchronous while staying single-threaded. It coordinates the call stack, microtask queue, and task queue in a fixed repeatable cycle. Understanding this architecture is the foundation for writing non-blocking code and debugging async timing issues.