The Callback Queue And Event Loop
How does JavaScript handle work that takes time? It doesn't. It delegates it to the browser.
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
- Browser APIs: Features like timers that run outside of the JavaScript engine.
- Callback Queue (Task Queue): Where functions wait once their background work is complete.
- The Event Loop: The "triage" system that decides when a function can move from the queue back to the stack.
- 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.
function printHello() {
console.log("Hello");
}
setTimeout(printHello, 1000);
console.log("Me first!");The Trace
- Line 1: Define
printHelloin global memory. - 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
printHellofunction to run on completion. - JavaScript completes the line and moves on immediately.
- Line 7: Console log "Me first!".
- In global memory, we log to the console at roughly 1ms.
- At 1000ms: The browser's timer completes.
- The browser can't just push
printHelloonto the stack; it might interrupt other code. - Instead, it pushes
printHellointo the Callback Queue.
- The browser can't just push
- The Event Loop: It checks: Is the stack empty? Yes. Is all global code done? Yes.
- It dequeues
printHelloand pushes it onto the Call Stack. - "Hello" is logged.
- It dequeues
The 0ms Challenge
What if we set the timer to 0 milliseconds?
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
printHelloenters the Callback Queue. - However, the thread of execution is busy running
blockFor300msand then the globalconsole.log. - The Event Loop strictly follows the rule: Nothing from the queue enters the stack until global code is finished.
Step 1:setTimeout(0) spins up a browser timer. It completes immediately and printHello enters the CALLBACK QUEUE.
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
- Video: The Event Loop Visualized
- Concept: Callback Queue vs Call Stack
- Course: JavaScript: The Hard Parts (The Event Loop)
Keep reading