Understanding the Event Loop, Task Queue, and Microtask Queue in JavaScript
JavaScript is an incredibly versatile language, known for its single-threaded, non-blocking nature. While this might sound counterintuitive for handling multiple tasks simultaneously, JavaScript employs a clever system involving the event loop, task queue, and microtask queue to manage asynchronous operations efficiently. In this blog, we’ll embark on a journey to explore these concepts, demystifying the complexities with practical examples and a touch of excitement.
1. Event Loop:
The event loop is the heart of JavaScript’s concurrency model. It continuously checks the call stack and task queues, ensuring that the execution of code remains non-blocking. It follows a simple yet effective principle: execute code from the call stack, and when the call stack is empty, fetch tasks from the task, microtask queue, and push them onto the call stack.
2. Task Queue:
The task queue (also known as the callback queue) holds tasks that are ready to be executed. These tasks usually originate from asynchronous operations like setTimeout(), DOM events, or Fetch requests. When a task is completed, it’s placed in the task queue to be picked up by the event loop for execution.
3. Microtask Queue:
The microtask queue holds tasks that are prioritized over tasks in the task queue. Microtasks include promises and mutation observer callbacks. When the call stack is empty and before fetching tasks from the task queue, the event loop first processes all tasks in the microtask queue. This ensures that microtasks are executed as soon as possible.
Don't be panicky, I will try to give many practical examples to differentiate between task and microtask.
Let's start with the basics and we will move forward to a more complex example.
Example 1: Asynchronous setTimeout
console.log("Start");
setTimeout(() => {
console.log("Inside setTimeout");
}, 100);
console.log("End");
Output
start
End
Inside setTimeout
Explanation
Start
andEnd
are logged synchronously.Inside setTimeout
is logged asynchronously after 100 ms when the call stack is empty, demonstrating the behavior of the event loop fetching tasks from the task queue.
It's easy to grasp it until now. Let's take another example.
Example 2: Asynchronous setTimeout and Task queue
console.log("Start");
setTimeout(() => {
console.log("Inside setTimeout");
}, 0);
console.log("End");
Output
start
End
Inside setTimeout
What ???? still, the output is the same. when we using setTimeout this time with zero which means it should execute immediately. right?
Explanation
In JavaScript, setTimeout
is an asynchronous function that schedules a task to be executed after a specified delay, given in milliseconds. When you call setTimeout
with a delay of 0
milliseconds, it doesn't mean the callback function passed to setTimeout
will be executed immediately; instead, it means it will be scheduled to run after the current execution context is complete, and the specified delay has elapsed.
Let’s break down the code:
console.log("Start");
This line prints "Start" to the console.
setTimeout(() => { console.log("Inside setTimeout"); }, 0);
This line schedules the callback function to be executed after a delay of 0
milliseconds. However, it doesn't mean that the callback will execute immediately. It will be added to the Task queue, and JavaScript will continue executing the rest of the synchronous code.
console.log("End");
This line prints "End" to the console.
Now, even though setTimeout
is scheduled with a delay of 0
milliseconds, JavaScript prioritizes executing synchronous code first. So, "Start" and "End" will be printed immediately because they are synchronous.
After the synchronous code execution completes, Event Loop checks the task queue for any pending tasks. Since the setTimeout
callback is waiting in the task queue, it's picked up for execution. This is why "Inside setTimeout" is logged to the console after "End".
Example 3: Promises and Microtask Queue
console.log("Start");
Promise.resolve().then(() => {
console.log("Promise resolved");
});
console.log("End");
Output
start
End
Promise resolved
Output Explanation
Start
andEnd
are logged synchronously.Promise resolved
is logged asynchronously before the next task from the microtask queue.
we don't have anything in the task queue in this example so let's take another example of mixing the task queue and microtask queue.
Example 4: Mixing Asynchronous Operations
console.log("Start");setTimeout(() => {
console.log("Inside setTimeout");
}, 0);Promise.resolve().then(() => {
console.log("Promise resolved");
});console.log("End");
Output
start
End
Promise resolved
Inside setTimeout
Output Explanation
Start
andEnd
are logged synchronously.Promise resolved
is logged beforeInside setTimeout
, demonstrating the microtask queue's priority over the task queue.Inside setTimeout
is logged after all synchronous tasks are complete and the microtask queue is empty.
okay so I think until now we have understood the difference between task and microtask queue very well.
Let's take a final and most complex example to make concepts more clear .
Example 5: The Grand Finale
async function check() {
await Promise.resolve(console.log(1));
console.log(2);
}
console.log(3);
check();
console.log(4);
output
3
4
1
2
So you thought this would be the output right? Looks easy as we understood. But here is a twist.
The actual output is
3
1
4
2
Any guess? why things have changed in this example? According to javascript first all the synchronous code should run so in this case why 1 is printed first?
Okay, let's go to explanation now.
- When
check()
is called, the execution of the function begins. console.log(1)
is encountered insideawait Promise.resolve()
. It's important to understand thatconsole.log(1)
is executed immediately when encountered, synchronously becauseconsole.log(1)
is an argument to thePromise.resolve()
expression and is therefore evaluated before the promise is even created.Promise.resolve(console.log(1))
is then called. This wraps the return value ofconsole.log(1)
, which isundefined
, into a resolved promise.- The promise thingy will be pushed to microtask queue and will be called when the call stack is empty.
- JavaScript will continue executing the rest of the synchronous code which is
console.log(4)
- after printing 4, then
console.log(2)
is pushed in the call stack and it will be executed. - so the final output is 3 , 1, 4, 2
Conclusion:
Understanding the event loop, task queue, and microtask queue is crucial for writing efficient asynchronous JavaScript code. By leveraging these mechanisms effectively, developers can ensure smooth and non-blocking execution of code, even in the face of asynchronous operations.