The Callback Queue And Event Loop

How does JavaScript handle work that takes time? It doesn't. It delegates it to the browser.

April 25, 20264 min read2 / 5

In the last part, we saw that JavaScript is single-threaded and blocking. To solve this, we use Facade Functions to communicate with the web browser. The most classic example of this is setTimeout.

The Essentials

  1. Browser APIs: Features like timers that run outside of the JavaScript engine.
  2. Callback Queue (Task Queue): Where functions wait once their background work is complete.
  3. The Event Loop: The "triage" system that decides when a function can move from the queue back to the stack.
  4. The Priority Rule: Callback functions cannot run until the Call Stack is empty and ALL global code has finished.

The Browser Interface: setTimeout

When we call setTimeout, we might think we're "waiting" in JavaScript. But we aren't. We're telling the browser to start a timer on our behalf.

JavaScript
function printHello() { console.log("Hello"); } setTimeout(printHello, 1000); console.log("Me first!");

The Trace

  1. Line 1: Define printHello in global memory.
  2. Line 5: Call setTimeout. This is a facade function.
    • It speaks to the browser's Timer API.
    • A timer is spun up in the background for 1000ms.
    • The browser keeps a reference to the printHello function to run on completion.
    • JavaScript completes the line and moves on immediately.
  3. Line 7: Console log "Me first!".
    • In global memory, we log to the console at roughly 1ms.
  4. At 1000ms: The browser's timer completes.
    • The browser can't just push printHello onto the stack; it might interrupt other code.
    • Instead, it pushes printHello into the Callback Queue.
  5. The Event Loop: It checks: Is the stack empty? Yes. Is all global code done? Yes.
    • It dequeues printHello and pushes it onto the Call Stack.
    • "Hello" is logged.

The 0ms Challenge

What if we set the timer to 0 milliseconds?

JavaScript
setTimeout(printHello, 0); blockFor300ms(); // A long loop console.log("Me first!");

Even though the timer is 0ms, printHello will not run immediately.

  • At 0ms, the timer completes and printHello enters the Callback Queue.
  • However, the thread of execution is busy running blockFor300ms and then the global console.log.
  • The Event Loop strictly follows the rule: Nothing from the queue enters the stack until global code is finished.
JavaScript Execution Engine
Thread of Execution
1function printHello() { console.log("Hello"); }
2setTimeout(printHello, 0);
3blockFor300ms();
4console.log("Me first!");

Step 1:setTimeout(0) spins up a browser timer. It completes immediately and printHello enters the CALLBACK QUEUE.

Memory
printHellof
Call Stack
Global
Bottom of Stack

Why Predictability Matters

This rule might seem annoying, but it's vital for predictability. If browser work could jump into our thread at any moment, we could never be sure what our memory state is. By forcing all "completed" background work to wait in a queue, JavaScript ensures that our synchronous code always runs to completion without interruption.

But what if we want to track that background work more closely? What if we want a placeholder for the data before it even arrives? In the next part, we'll see the ES6 solution: Promises.

Further Reading and Watching