The Event Loop
JavaScript is a single-threaded, non-blocking, asynchronous, concurrent language. It has a single Call Stack and Memory Heap. The Event Loop is the secret sauce that allows JavaScript to perform non-blocking I/O operations despite being single-threaded.
The Environment
The JavaScript runtime (like V8) doesn't work alone. In a browser, it sits alongside Web APIs (like DOM, AJAX, setTimeout), and in Node.js, it sits alongside C++ APIs.
1. Memory Heap
The memory heap is where memory allocation happens. Objects are allocated in a heap which is just a name to denote a large mostly unstructured region of memory.
2. Call Stack
The Call Stack is a data structure that records where we are in the program. If we step into a function, we put it on the top of the stack. If we return from a function, we pop off the top of the stack.
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
var squared = square(n);
console.log(squared);
}
printSquare(4);
Stack trace: printSquare -> square -> multiply -> console.log.
3. Web APIs
Web APIs are threads that you can't access directly, provided by the browser (or Node's internal APIs). They handle tasks like HTTP requests, timers, and DOM events. When a task is completed, it pushes the callback function to a queue.
4. Callback/Task Queue (Macrotask Queue)
When Web APIs finish their execution, they push their callback functions to the Task Queue (e.g., setTimeout, setInterval, DOM events).
5. Microtask Queue
Introduced for Promises and MutationObserver. The Microtask Queue has a higher priority than the Macrotask Queue. After the call stack is empty, the Event Loop processes all microtasks before moving on to the next macrotask.
How the Event Loop Works
The Event Loop has one simple job: monitor the Call Stack and the Callback Queues.
- Execute the Call Stack: If there are frames on the stack, execute them until the stack is empty.
- Process Microtasks: Once the Call Stack is empty, check the Microtask Queue. If there are microtasks (like
.then()callbacks), dequeue and execute them one by one until the Microtask Queue is completely empty. - Render (Browser only): If a render is due, update the rendering pipeline.
- Process a Macrotask: Take the first task from the Macrotask Queue (Callback Queue) and push it onto the Call Stack to execute.
- Repeat: Go back to step 1.
Code Example: Guess the Output
console.log('1. Script start');
setTimeout(() => {
console.log('2. setTimeout (Macrotask)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise 1 (Microtask)');
}).then(() => {
console.log('4. Promise 2 (Microtask)');
});
console.log('5. Script end');
Output:
1. Script start(Synchronous, Call Stack)5. Script end(Synchronous, Call Stack)3. Promise 1 (Microtask)(Microtask Queue processed first)4. Promise 2 (Microtask)(Chained microtask processed immediately after)2. setTimeout (Macrotask)(Macrotask Queue processed last)
Why is this important?
Understanding the event loop prevents you from writing blocking code.
- If you have an infinite
whileloop or heavy synchronous computation, it blocks the Call Stack. - If the Call Stack is blocked, the browser cannot render, and you cannot process clicks or network responses.
- Rule of thumb: Keep the Call Stack clear. Use asynchronous Web APIs to offload heavy lifting or delay operations.