How Event Loops Work

The secret behind JavaScript's non-blocking magic. Visualize the Call Stack, Task Queue, and Microtasks.

Call Stack
main()
console.log("A")
console.log("B")
Web APIs
Timer done
Microtask Queue
(empty)
Task Queue (Macrotasks)
(empty)
console.log("A")
1 / 10

Synchronous Execution

The Call Stack

What Happens

JavaScript is single-threaded. Functions are pushed onto the Call Stack and executed one at a time.

Why

Simple mental model. No race conditions within a single thread.

Technical Detail

LIFO (Last In, First Out). Each function call creates a "stack frame".

Example console.log("A"); console.log("B"); → Output: A, B

The Engineering of the Event Loop: Concurrency in a Single Thread

JavaScript is a single-threaded programming language. It possesses exactly one Call Stack and one Memory Heap. It cannot execute two functions mathematically at the same exact time. So how can a Node.js server handle 10,000 concurrent HTTP requests fetching data from a database without freezing entirely? The secret lies outside of the V8 engine itself—in an orchestration mechanism known as the Event Loop.


Part 1: The Tyranny of the Call Stack

The V8 JavaScript Engine relies on a Call Stack. When a function is invoked, a "Stack Frame" containing its local variables and execution context is pushed to the top of the stack. When the function returns, it is popped off.

Because JavaScript is single-threaded, if a function takes 5 seconds to execute (e.g., a massive while loop calculating Fibonacci numbers), the Single Thread is physically blocked. The browser cannot render UI updates, and it cannot process click events. To the user, the entire web page appears frozen (Jank) because the Call Stack is gridlocked.

This means we cannot perform Network Requests sequentially on the main thread. If we execute fetch('api/data'), we cannot simply halt the V8 engine for 200 milliseconds waiting for a TCP response from a remote server.

Part 2: The Web APIs Sandbox

When you call setTimeout or fetch, you are not actually calling V8 JavaScript functions. You are invoking bindings to C++ APIs provided by the hosting environment (either the Browser's Web APIs or Node.js's libuv library).

If you execute setTimeout(myCallback, 5000):

  1. The V8 engine pushes setTimeout to the Call Stack.
  2. V8 instantly hands the timer logic over to the Browser's C++ Timer API.
  3. V8 immediately pops setTimeout off the Call Stack and proceeds to the next line of code. The Single Thread is instantly freed.
  4. In the background, on a completely separate, invisible C++ thread, the Browser counts down 5 seconds.

But when the 5 seconds are up, the Browser cannot simply forcefully inject myCallback directly into the V8 Call Stack. If it did, it would arbitrarily interrupt whatever JavaScript was currently executing, causing catastrophic memory corruption. We need an orderly queue.

Part 3: The Task Queues

When background operations (timers, network requests, DOM click events) finish, the Browser places their associated JavaScript callbacks into a waiting area called the Task Queue (or Macrotask Queue).

Simultaneously, Promises were introduced to JavaScript to handle asynchronous values. Because resolving a Promise is often considered highly critical application logic, Promise callbacks (.then() or .catch()) bypass the standard Task queue and are placed into an exclusive, high-priority lane called the Microtask Queue.

// The classic interview question
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2

Part 4: The Core Algorithm

The Event Loop is merely an infinite `while(true)` loop running endlessly inside the Javascript engine. Its singular purpose is to act as a traffic cop, moving callbacks from the Queues into the empty Call Stack.

On every single tick, the Event Loop executes this exact mathematical algorithm:

  • Step 1: Check Stack. Is the Call Stack completely empty? If no, wait. (The Event loop can do absolutely nothing while synchronus code is running).
  • Step 2: Flush Microtasks. If the Call Stack is empty, look at the Microtask Queue (Promises). If there are items, execute every single one of them continuously until the Microtask Queue is completely empty.
  • Step 3: Render (Browser only). Check if the UI needs to repaint the screen (typically running requestAnimationFrame callbacks to maintain 60 FPS).
  • Step 4: Execute ONE Macrotask. Look at the Task Queue (setTimeout, DOM events). Take exactly one callback and push it onto the Call Stack to execute.
  • Step 5: Loop. Return to Step 1.

Part 5: Starvation and Deadlocks

Notice the critical difference between Step 2 and Step 4. Step 4 processes exactly one Macrotask per cycle, yielding control back to the Event Loop so the browser can render. Step 2 processes all Microtasks until the queue is empty.

What happens if a Microtask queues another Microtask?

function recursivelyPromise() {
  Promise.resolve().then(recursivelyPromise);
}
recursivelyPromise();

This is a subtle but catastrophic failure called Microtask Starvation. The Event Loop begins executing Step 2. It resolves the Promise, but the callback immediately pushes a new Promise onto the Microtask Queue. The Event Loop is trapped in Step 2. It can never proceed to Step 3 (Rendering) or Step 4 (Macrotasks). The Call Stack never overflows (like a standard infinite loop), but the browser completely freezes because the Event Loop is deadlocked in the Microtask phase forever.

Conclusion: The Single-Threaded Illusion

JavaScript itself may be rigidly single-threaded, but the environment it runs within is aggressively multi-threaded. By offloading heavy I/O operations—database queries, file system writes, and network handshakes—to background C++ thread pools, and meticulously choreographing the resulting callbacks through prioritized queues, the Event Loop achieves a masterful illusion of unbounded parallel concurrency.

Glossary & Concepts

Call Stack

The data structure that tracks function execution. LIFO (Last In, First Out). Must be empty for the Event Loop to pick new tasks.

Task Queue (Macrotasks)

Where async callbacks wait. Includes setTimeout, setInterval, I/O, user events. One task per Event Loop cycle.

Microtask Queue

Higher priority queue. Promises, queueMicrotask(), MutationObserver. ALL microtasks drain before the next macrotask.

Event Loop

The infinite cycle that orchestrates JS execution. Checks stack → runs microtasks → runs one task → renders → repeat.

Web APIs

Browser-provided APIs (setTimeout, fetch, DOM) that handle async work off the main thread.

Starvation

When microtasks continuously add more microtasks, blocking rendering and macrotasks indefinitely.