Event Loop in JavaScript

Event Loop is a fundamental concept in JavaScript that enables asynchronous programming. This blog post explores how the event loop works and its significance in managing tasks and callbacks.
The event loop is responsible for handling asynchronous operations in JavaScript, allowing non-blocking execution of code. It works in conjunction with the call stack, web APIs, the micro and macro task queue to manage the execution of functions.
How the Event Loop Works
- Call Stack: The call stack is a data structure that keeps track of the function calls in a program. When a function is invoked, it is added to the top of the stack. When the function completes, it is removed from the stack.
- Web APIs: When an asynchronous operation (like a timer or an HTTP request) is initiated, it is handled by the browser's Web APIs. These operations run in the background, allowing the main thread to continue executing other code.
- Micro Task Queue: When certain asynchronous operations (like Promises) are completed, their callbacks are added to the micro task queue. The event loop gives priority to the micro task queue over the macro task queue.
- Macro Task Queue: Some asynchronous operations, like setTimeout and setInterval, use the macro task queue. Callbacks from these operations are added to the macro task queue once they are ready to be executed.
- Event Loop: The event loop continuously monitors the call stack and the task queue. If the call stack is empty, the event loop takes the first callback from the task queue and pushes it onto the call stack for execution. This process ensures that asynchronous callbacks are executed only when the main thread is free.
Micro Tasks vs. Macro Tasks
Micro tasks are Promises, queueMicrotask, Mutation Observers, and process.nextTick in Node.js. Macro tasks include setTimeout, setInterval, setImmediate, I/O operations, and UI rendering.
Golden Rule of Event Loop
In Promises, then, catch, and finally each schedule a new microtask, and every await also schedules a new microtask.
Let's see an example to illustrate how the event loop works in JavaScript.
Example
console.log("Start"); //Macro Task setTimeout(() => { console.log("Timeout Callback"); }, 0); //Micro Task Promise.resolve().then(() => { console.log("Promise Callback"); }); console.log("End");
So the output of the above code will be:
Output:
Start End Promise Callback Timeout Callback
Explanation:
- "Start" is logged to the console immediately.
- A
setTimeoutis set with a delay of 0 milliseconds, which adds the callback to the macro task queue. - A resolved Promise is created, which adds its callback to the micro task queue.
- "End" is logged to the console immediately after.
- The event loop checks the call stack and finds it empty, so it processes the micro task queue first, logging "Promise Callback".
- Finally, the event loop processes the macro task queue, logging "Timeout Callback".
but what about:
new Promise((resolve) => { console.log("4"); //this is synchronous resolve(null); }).then(() => { console.log("5"); //this is asynchronous (micro task) }); console.log("6");
The output of the above code will be:
4 6 5
Explanation:
- "4" is logged to the console immediately when the Promise is created.
- The
thencallback is added to the micro task queue. - "6" is logged to the console immediately after.
- The event loop checks the call stack and finds it empty, so it processes the micro task queue, logging "5".
let's see another example:
(async () => { console.log("8"); //this is synchronous await null; //this is asynchronous (micro task) so rest of the function will be added to micro task queue console.log("9"); })(); console.log("10"); //this is synchronous
The output of the above code will be:
8 10 9
Explanation:
- "8" is logged to the console immediately when the async function is invoked.
- The
await nullcauses the function to pause, and the rest of the function is added to the micro task queue. - "10" is logged to the console immediately after.
- The event loop checks the call stack and finds it empty, so it processes the micro task queue, logging "9".
what about this one:
queueMicrotask(() => { console.log("6"); //this is asynchronous (micro task) queueMicrotask(() => { console.log("7"); //this is asynchronous (micro task) }); }); console.log("8"); //this is synchronous
The output of the above code will be:
8 6 7
Explanation:
- The first
queueMicrotaskcallback is added to the micro task queue. - "8" is logged to the console immediately after.
Conclusion
The event loop is a crucial part of JavaScript's concurrency model, enabling asynchronous programming by managing the execution of tasks. Understanding how the event loop works, along with the call stack, web APIs, and task queues, is essential for writing efficient and responsive JavaScript applications. By leveraging the event loop effectively, developers can create non-blocking code that enhances user experience and application performance.