Understanding the Event Loop, Task Queue, and Microtask Queue in JavaScript

Rehmat Sayany
5 min readMar 23, 2024
Javascript visualizer.

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

  1. Start and End are logged synchronously.
  2. 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

  1. Start and End are logged synchronously.
  2. 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

  1. Start and End are logged synchronously.
  2. Promise resolved is logged before Inside setTimeout, demonstrating the microtask queue's priority over the task queue.
  3. 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.

  1. When check() is called, the execution of the function begins.
  2. console.log(1) is encountered inside await Promise.resolve(). It's important to understand that console.log(1) is executed immediately when encountered, synchronously because console.log(1) is an argument to the Promise.resolve() expression and is therefore evaluated before the promise is even created.
  3. Promise.resolve(console.log(1)) is then called. This wraps the return value of console.log(1), which is undefined, into a resolved promise.
  4. The promise thingy will be pushed to microtask queue and will be called when the call stack is empty.
  5. JavaScript will continue executing the rest of the synchronous code which is console.log(4)
  6. after printing 4, thenconsole.log(2) is pushed in the call stack and it will be executed.
  7. 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.

--

--

Rehmat Sayany

Full Stack developer @westwing passionate about NodeJS, TypeScript, React JS and AWS.