Understanding the javascript event loop
- #javascript
- #runtime
- #fundamentals
Understanding the JavaScript Event Loop: The Definitive Guide
JavaScript is a single-threaded language. This means it has only one call stack (Call Stack) and can execute only one piece of code at a time.
Yet, modern JavaScript applications smoothly handle network requests, user clicks, and complex animations simultaneously without freezing the UI. This architectural feat relies entirely on a central mechanism: the Event Loop.
1. The 4 Core Components of the Concurrency Model
To understand how asynchronous code is orchestrated, you need to visualize four components working together behind the scenes:
- The Call Stack: This is the primary engine. Functions are pushed onto the stack when called and popped off once they return a value. It operates on a LIFO (Last In, First Out) basis.
- Web APIs (or Runtime Environment): Provided by the browser (or Node.js), these handle heavy background tasks like
fetch(),setTimeout(), and DOM event listeners. - The Microtask Queue: A high-priority queue that stores callbacks from promises (
.then(),catch(),finally()) andasync/awaitoperations. - The Task Queue (or Macro-task Queue): A standard-priority queue that stores callbacks from DOM events,
setTimeout(), andsetInterval().
2. How the Event Loop Works
The Event Loop is a continuous loop whose sole job is to monitor the Call Stack. It follows a strict, golden rule:
- Execute Synchronous Code: The Event Loop waits until the Call Stack is completely empty.
- Clear the Microtask Queue: As soon as the stack clears, the Event Loop checks the Microtask Queue and executes all pending microtasks sequentially until the queue is entirely empty.
- Process One Macro-task: Once the Microtask Queue is completely clear, the Event Loop picks only one macro-task from the Task Queue and pushes it to the Call Stack for execution.
- Repeat: The cycle starts all over again.
3. Practical Example and Execution Order
Let's break down the execution flow of this classic code snippet:
console.log('1: Start of script');
setTimeout(() => {
console.log('2: setTimeout callback (Macro-task)');
}, 0);
Promise.resolve().then(() => {
console.log('3: Promise callback (Micro-task)');
});
console.log('4: End of script');Console Output:
1: Start of script
4: End of script
3: Promise callback (Micro-task)
2: setTimeout callback (Macro-task)Why this specific order?
- Messages
1and4are synchronous. They are pushed directly to the Call Stack and execute immediately. - The
setTimeoutdelegates its countdown to the Web API, which instantly moves its callback to the Task Queue (macro-task). - The resolved promise places its
.then()callback into the Microtask Queue. - Once the synchronous code finishes (the Call Stack becomes empty), the Event Loop processes the Microtask Queue first, printing message
3. - Finally, the Event Loop picks up the remaining macro-task, printing message
2.
4. Why This Matters for Developers
- Preventing Main Thread Blocking: If you run a heavy mathematical computation synchronously, the Call Stack stays occupied. The Event Loop freezes, causing the user interface to hang or lag.
- Predictable Asynchronous Code: Understanding the Event Loop allows you to confidently predict code execution order and prevent race conditions or bugs related to data timing.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CALL STACK โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ (When empty)
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MICROTASK QUEUE (Promises) โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ (Once completely empty)
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TASK QUEUE (setTimeout, DOM) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโIn Summary
JavaScript simulates parallelism through the Event Loop. By delegating time-consuming tasks to the runtime environment and prioritizing execution using separate queues (Micro vs. Macro), JavaScript remains exceptionally fast, responsive, and non-blocking.