What is the event loop?
The JavaScript Event Loop is a fundamental concurrency model that allows JavaScript, despite being single-threaded, to perform non-blocking I/O operations and handle asynchronous tasks. It's crucial for understanding how JavaScript executes code, especially when dealing with operations like network requests, timers, and user interactions.
What is the Event Loop?
At its core, JavaScript is a single-threaded language, meaning it can only execute one piece of code at a time. To manage asynchronous operations without blocking the main thread (and thus freezing the user interface), the JavaScript runtime environment (like a browser or Node.js) utilizes the event loop. The event loop continuously monitors the Call Stack and the Callback Queue (or Task Queue) to determine what code to execute next.
Key Components of the Event Loop
- Call Stack: A LIFO (Last In, First Out) stack that stores function calls. When a function is called, it's pushed onto the stack. When it returns, it's popped off. All synchronous JavaScript code executes here.
- Web APIs (Browser Environment) / C++ APIs (Node.js): These are functionalities provided by the runtime environment, not JavaScript itself. Examples include
setTimeout(),DOMevents (click,load),fetch(),XMLHttpRequest. When an asynchronous function is called, it's passed to these APIs to handle. - Callback Queue (Task Queue / Macro-task Queue): After a Web API completes its asynchronous task (e.g., a timer expires, a network request finishes), its associated callback function is placed into this queue, waiting to be executed. Examples:
setTimeout,setInterval,setImmediate(Node.js), I/O, UI rendering. - Micro-task Queue: A separate queue for 'micro-tasks' that have higher priority than macro-tasks. Callbacks from Promises (
.then(),.catch(),.finally()) andqueueMicrotask()are placed here. - The Event Loop: The mechanism that constantly checks if the Call Stack is empty. If it is, it first checks the Micro-task Queue. If there are micro-tasks, it moves all of them to the Call Stack and executes them. Only after the Micro-task Queue is empty does it then check the Callback Queue. If there are tasks there, it takes the first one and pushes it onto the Call Stack for execution.
How it Works (Simplified Flow)
- Synchronous code runs on the Call Stack. If it's a function, it's pushed; if it returns, it's popped.
- When an asynchronous function (e.g.,
setTimeout,fetch,Promise) is encountered, it's passed to the appropriate Web API (or equivalent runtime API). The JavaScript engine doesn't wait for it; it continues executing subsequent synchronous code. - Once the asynchronous operation completes, its callback function is not immediately executed. Instead, it's placed into either the Callback Queue (for macro-tasks) or the Micro-task Queue (for micro-tasks).
- The Event Loop continuously checks if the Call Stack is empty. If it is, it first drains the entire Micro-task Queue, moving each micro-task callback to the Call Stack to be executed.
- After the Micro-task Queue is empty, the Event Loop then checks the Callback Queue. If there are pending callbacks, it dequeues the first one and pushes it onto the Call Stack for execution.
- This process repeats indefinitely, allowing JavaScript to handle concurrency without blocking the main thread.
Macro-tasks vs. Micro-tasks Priority
A crucial aspect of the event loop is the priority given to micro-tasks over macro-tasks. After the Call Stack is empty, the event loop will process ALL micro-tasks in the micro-task queue before picking up a single macro-task from the macro-task queue. This means that a long chain of promises can delay the execution of a setTimeout callback.
Example Code
console.log('1. Start');
setTimeout(() => {
console.log('4. setTimeout callback');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise microtask');
});
console.log('2. End');
Output of the above code:
1. Start
2. End
3. Promise microtask
4. setTimeout callback
Explanation:
- '1. Start' and '2. End' are synchronous and execute immediately.
- setTimeout's callback goes into the Callback Queue.
- Promise.resolve().then()'s callback goes into the Micro-task Queue.
- After synchronous code finishes, the Call Stack is empty.
- The Event Loop checks the Micro-task Queue, finds the Promise callback, moves it to the Call Stack, and '3. Promise microtask' prints.
- The Micro-task Queue is now empty.
- The Event Loop checks the Callback Queue, finds the setTimeout callback, moves it to the Call Stack, and '4. setTimeout callback' prints.