7. The Microtask Queue
The microtask queue is an integral part of JavaScript’s event-driven architecture, and it works closely with the event loop and callback queue to manage the execution of tasks. While the callback queue handles tasks such as setTimeout
and I/O operations, the microtask queue is reserved for microtasks—smaller, high-priority tasks that must be executed as soon as the current operation completes.
What is the Microtask Queue?
The microtask queue is a special queue that holds microtasks, which are high-priority tasks that need to be executed after the current operation completes but before any tasks from the callback queue are processed. Microtasks are typically generated by promises, MutationObserver
, and other asynchronous APIs.
The microtask queue allows for more fine-grained control over task execution, ensuring that certain operations, like promise resolutions, are handled promptly before moving on to other tasks.
How the Microtask Queue Works
The microtask queue operates in tandem with the event loop. Here’s how it works:
The event loop checks the call stack:
- The event loop first checks if the call stack is empty. If it is not, it waits until all operations on the call stack are completed.
The event loop processes the microtask queue:
- Once the call stack is empty, the event loop checks the microtask queue. If there are any tasks in the microtask queue, they are pushed onto the call stack for immediate execution.
The event loop processes the callback queue:
- Only after the microtask queue is completely empty does the event loop move on to the callback queue to process tasks like
setTimeout
callbacks.
- Only after the microtask queue is completely empty does the event loop move on to the callback queue to process tasks like
This sequence ensures that microtasks are always given priority over macrotasks (tasks in the callback queue), which allows for prompt handling of certain operations, such as the resolution of promises.
Example of the Microtask Queue in Action
Let’s explore a practical example involving promises, which are a common source of microtasks:
console.log('Start');
Promise.resolve().then(() => {
console.log('Promise callback 1');
});
Promise.resolve().then(() => {
console.log('Promise callback 2');
});
setTimeout(() => {
console.log('Timeout callback');
}, 0);
console.log('End');
Explanation:
- The
Promise.resolve().then()
calls create microtasks, which are placed in the microtask queue. - The
setTimeout
function schedules a callback to run after 0 milliseconds, which is placed in the callback queue (macrotask queue). - The event loop first executes the synchronous code, logging “Start” and “End”.
- It then processes the microtasks before moving on to the macrotask queue, so the promise callbacks are executed before the
setTimeout
callback. - The output will be:
Start
End
Promise callback 1
Promise callback 2
Timeout callback
This demonstrates how microtasks are executed before any macrotasks, even if the macrotasks are scheduled to run immediately.
Microtasks vs. Macrotasks
Microtasks have higher priority than macrotasks. This distinction is crucial when working with asynchronous operations because it affects the order in which tasks are executed.
Microtasks:
- Generated by: Promises,
MutationObserver
,queueMicrotask
. - Executed immediately after the current operation completes, before any macrotasks.
Macrotasks:
- Generated by:
setTimeout
,setInterval
, I/O operations,setImmediate
(in Node.js). - Executed after all microtasks are completed and the call stack is empty.
This priority ensures that microtasks, like promise resolutions, are handled as soon as possible, allowing for a responsive and efficient application.
Practical Application: Handling Promises
Promises are one of the most common sources of microtasks. Understanding the microtask queue helps you manage promise resolutions and their callbacks effectively.
Example:
async function fetchData() {
console.log('Fetch data start');
const data = await new Promise((resolve) => {
setTimeout(() => {
resolve('Data received');
}, 2000);
});
console.log(data);
}
fetchData();
Promise.resolve().then(() => {
console.log('Promise callback during fetch');
});
console.log('End of script');
Explanation:
- The
fetchData
function is asynchronous and waits for the promise to resolve, simulating a 2-second delay. - During this time, the
Promise.resolve().then()
microtask is executed, demonstrating that microtasks are handled even while awaiting the resolution of another promise. - The output will be:
Fetch data start
End of script
Promise callback during fetch
Data received
This example illustrates how the microtask queue manages promise resolutions, ensuring that they are processed as soon as possible, even within asynchronous functions.
Using queueMicrotask
The queueMicrotask
function allows you to manually add a microtask to the microtask queue. This can be useful for scheduling small tasks that need to be executed after the current operation but before any macrotasks.
Example:
console.log('Start');
queueMicrotask(() => {
console.log('Microtask');
});
console.log('End');
Explanation:
- The
queueMicrotask
schedules the microtask to run after the current operation. - The output will be:
Start
End
Microtask
This shows how queueMicrotask
can be used to control the timing of operations in your code.
Summary:
The microtask queue is a vital component of JavaScript’s event loop, ensuring that high-priority tasks, like promise resolutions, are executed promptly. By understanding how the microtask queue interacts with the event loop and the callback queue, you can write more efficient and responsive code.