The Microtask Queue

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:

  1. 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.
  2. 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.
  3. 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.

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:

javascript
	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:
txt
	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:

javascript
	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:
txt
	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:

javascript
	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:
txt
	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.