Introduction
JavaScript runs on a single thread — it can only do one thing at a time — yet it manages to juggle asynchronous operations like fetching data from a server or responding to user interactions without making the page unresponsive. The secret behind this coordination is the Event Loop, which decides when and how each task gets executed.
In this article, we’ll break down the event loop step by step — using clear visuals and GIFs — so you can see what happens inside the call stack, task queue, and microtask queue. By the end, you’ll understand exactly why your async code behaves the way it does.
What is the Event Loop?
The event loop is the mechanism that enables JavaScript to handle asynchronous operations while remaining single-threaded. It ensures non-blocking execution by coordinating between the call stack, web APIs, callback queues, and microtask queues.
Now, let’s take a closer look at each part that makes this process work.
Components of the Event Loop
To understand the event loop better, let’s break it into its key components:
Call Stack - The call stack is where JavaScript keeps track of function calls. Functions are pushed onto the stack when invoked and popped off when they finish execution.
Web APIs/Node APIs - These provide the environment for asynchronous tasks like
setTimeout()
,fetch
, and DOM events. They are managed outside the call stack.Callback Queue (Task Queue) - When an asynchronous operation completes, its callback is queued here to await execution.
Microtask Queue - Tasks like Promise resolutions and
MutationObserver
callbacks are queued here. This queue is prioritized over the callback queue.Event Loop - The event loop continuously checks if the call stack is empty and, if so, pushes the next task from the callback or microtask queue onto the stack.
How it works (Step-by-step example)
Let's consider the following code snippet to understand the event loop in action:
console.log("Start");
setTimeout(() => {
console.log("Timeout callback");
}, 0);
console.log("End");
The output will be:
Start
End
Timeout callback
At first glance, you might expect "Timeout callback" to appear immediately after "Start", but it doesn’t.
Here's what's happening under the hood:
The JavaScript engine starts by executing the synchronous code, line by line: The
console.log("Start")
function is pushed onto the call stack. It executes immediately, printing "Start" to the console. After execution, the function is popped off the stack.Next, the engine encounters the
setTimeout
function: ThesetTimeout
function is pushed onto the call stack. It registers the callback function with the Web API (provided by the browser or runtime like Node.js). After registering the callback,setTimeout
is popped off the stack. The callback function is now waiting in the Web API environment with a timer set to 0 milliseconds.The interpreter moves to the next line. The
console.log("End")
function is pushed onto the call stack. It executes immediately, printing "End" to the console. After execution, the function is popped off the stack.Once all synchronous code is executed, the call stack is empty. The event loop checks if there are any pending tasks in the task queue:
- The callback function from
setTimeout
is moved from the Web API to the task queue after the timer expires (0 milliseconds). - The event loop picks up the callback from the task queue and pushes it onto the call stack.
- The callback function executes, printing Timeout callback to the console.
- After execution, the function is popped off the stack.
Once the callback is executed and popped off the stack, the event loop moves on to the next task in the queue. But not all queued work is treated equally. JavaScript introduces another layer of complexity: Microtasks, primarily driven by Promises
, actually jump ahead of the regular task queue. That’s what we’ll explore next.
Microtask Queues
Microtasks are prioritized over tasks (like setTimeout
callbacks), which means they are executed immediately after synchronous code completes, even before tasks in the task queue.
To see this in action, consider this example:
console.log("Start");
setTimeout(() => {
console.log("Timeout callback");
}, 0);
Promise.resolve().then(() => {
console.log("Promise resolved");
});
console.log("End");
Let's break down the execution flow into the following steps:
console.log("Start")
is added to the Call Stack and executed immediately.console.log("Start")
runs and logs "Start".console.log("End")
runs and logs "End".The
setTimeout
is encountered. Its callback (console.log("Timeout callback")
) is handed off to the Web API and scheduled for execution after 0 milliseconds. Once the timer expires, the callback is moved to the task queue. The call stack is now empty.The
Promise.resolve()
creates a resolved promise. Its.then()
callback (console.log("Promise resolved")
) is added to the microtask queue for execution after all synchronous code.Synchronous code execution continues.
console.log("End")
is added to the call stack and executed immediately.Once the synchronous code finishes, the event loop checks the microtask queue. The
.then()
callback is executed (console.log("Promise resolved")
) and "Promise resolved" is logged to the console.After all microtasks are executed, the event loop moves to the task queue.
This brings us to the output:
Start
End
Promise resolved
Timeout callback
By prioritizing microtasks, JavaScript guarantees that promise resolutions or other immediate tasks are executed before moving on to other queued tasks, such as setTimeout
callbacks.
Ready to take the wheel?
Now that you understand the event loop, let's put that knowledge into practice! Try to predict the output of the following code snippet and compare it with the actual output:
console.log("Start");
setTimeout(function() {
console.log("setTimeout 1");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 1");
});
setTimeout(function() {
console.log("setTimeout 2");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 2");
});
console.log("End");
Conclusion
- JavaScript is single-threaded, but it uses the event loop to handle asynchronous operations.
- The call stack tracks function calls, and the event loop ensures that tasks from the callback and microtask queues are executed when the stack is empty.
-
Web APIs (like
setTimeout
,fetch
) manage asynchronous tasks and move their callbacks to the task queue once they complete. - Microtasks (like promises) are given priority over regular tasks, ensuring they are executed before tasks in the callback queue.
In short, the event loop is at the core of JavaScript's ability to handle asynchronous operations in a single-threaded environment. By understanding its components — such as the call stack, web APIs, callback queue, and microtask queue — you can gain deeper insights into how JavaScript executes tasks and maintains non-blocking behavior. Whether it's handling DOM events, promises, or timers, the event loop ensures smooth and responsive web applications.
Embrace the power of JavaScript's event loop, and you’ll be well on your way to mastering asynchronous programming! Happy coding!
Top comments (0)