Topic 000: Event Loop, Microtasks and Macrotasks Queue

To understand how JavaScript executes asynchronous code, it’s essential to grasp the concepts of microtasks and macrotasks.
Microtasks are tasks that are executed immediately after the currently executing script and before any rendering or macrotasks. They have higher priority over macrotasks and are processed first in the event loop. Common sources of microtasks include:
.then, .catch, .finally)(fetch from Node.js)process.nextTick (Node.js)queueMicrotask Functionawait Expressions in Async FunctionsMacrotasks are tasks that are executed after the current executing script and all microtasks have been processed. They include:
setTimeout and setInterval CallbacksXMLHttpRequest)addEventListener)Let’s look at an example JavaScript file that uses various microtasks and macrotasks to illustrate their execution order.
console.log("Script start");
// Macro Task: setTimeout
setTimeout(() => {
console.log("setTimeout");
}, 0);
// Micro Task: Promise
Promise.resolve()
.then(() => {
console.log("Promise 1");
})
.then(() => {
console.log("Promise 2");
});
// Macro Task: Fetch API
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => response.json())
.then((data) => {
console.log("Fetch API");
});
// Macro Task: setImmediate (Node.js only)
if (typeof setImmediate !== "undefined") {
setImmediate(() => {
console.log("setImmediate");
});
}
// Micro Task: process.nextTick (Node.js only)
if (typeof process !== "undefined" && process.nextTick) {
process.nextTick(() => {
console.log("process.nextTick");
});
}
// Micro Task: queueMicrotask
queueMicrotask(() => {
console.log("queueMicrotask");
});
// DOM Manipulation (Macro Task)
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM fully loaded and parsed");
});
console.log("Script end");
Microtasks:
process.nextTick (Node.js): Executed immediately after the current operation completes.queueMicrotask: Executed after process.nextTick.Promise 1: The first .then callback of the resolved promise.Promise 2: The chained .then callback.fetch response: console.log("Fetch API");setTimeout: Executed after all microtasks are completed.setImmediate (Node.js): Executed as a macrotask in Node.js.DOMContentLoaded: Executed when the initial HTML document has been completely loaded and parsed.Promise Callbacks:
Promise.resolve()
.then(() => {
console.log("Promise 1");
})
.then(() => {
console.log("Promise 2");
});
fetch Promise
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => response.json())
.then((data) => {
console.log("Fetch API");
});
Promises are resolved asynchronously. The .then and .catch callbacks are pushed onto the microtask queue.
Mutation Observer API:
const observer = new MutationObserver(() => {
console.log("Mutation observed");
});
observer.observe(document.body, { childList: true });
document.body.appendChild(document.createElement("div"));
The MutationObserver callback runs as a microtask when mutations are observed.
process.nextTick (Node.js):
if (typeof process !== "undefined" && process.nextTick) {
process.nextTick(() => {
console.log("process.nextTick");
});
}
process.nextTick queues a microtask in Node.js to run before any other microtask or macrotask.
queueMicrotask Function:
queueMicrotask(() => {
console.log("queueMicrotask");
});
queueMicrotask explicitly queues a microtask.
await Expressions in Async Functions:
(async () => {
await Promise.resolve();
console.log("Async/await");
})();
await in an async function pauses execution, and the subsequent code runs as a microtask.
setTimeout and setInterval Callbacks:
setTimeout(() => {
console.log("setTimeout");
}, 0);
setTimeout queues a macrotask to run after the specified delay.
DOM Manipulation and Rendering:
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM fully loaded and parsed");
});
The DOMContentLoaded event fires once the document is fully loaded and parsed, handled as a macrotask.
I/O Operations (File Reading/Writing):
const fs = require("fs");
fs.readFile("file.txt", (err, data) => {
if (err) throw err;
console.log("File read");
});
File operations queue macrotasks.
Network Requests (XMLHttpRequest):
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://example.com");
xhr.onload = () => {
console.log("XHR Load");
};
xhr.send();
Network requests queue macrotasks for their response handlers.
Event Handlers (addEventListener):
document.addEventListener("click", () => {
console.log("Document clicked");
});
Event listeners queue macrotasks when the event occurs.
The distinction between XMLHttpRequest (XHR) and fetch in terms of task scheduling (macrotask vs. microtask) is a fundamental part of how JavaScript’s event loop handles asynchronous operations.
Let’s dive deeper into the explanation with examples:
console.log("Start");
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1");
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
console.log("XHR response received");
}
};
xhr.send();
console.log("End");
In this example:
onreadystatechange callback is invoked, which is scheduled as a macrotask.console.log("Start");
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => response.json())
.then((data) => {
console.log("Fetch response received");
});
console.log("End");
In this example:
.then() callback is scheduled as a microtask.To clearly see the difference, we can compare how microtasks and macrotasks are handled with additional logs and setTimeout (which schedules macrotasks).
console.log("Start");
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1");
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
console.log("XHR response received");
}
};
xhr.send();
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => response.json())
.then((data) => {
console.log("Fetch response received");
});
setTimeout(() => {
console.log("setTimeout callback");
}, 0);
Promise.resolve().then(() => {
console.log("Promise resolved");
});
console.log("End");
Output Order:
setTimeout and XHR callbacks.Macrotasks (like setTimeout and XHR callbacks) are processed after the microtasks. Hence, “setTimeout callback” and “XHR response received” appear later in the log.
fetch API, which returns promises, operates in the microtask queue, while the XHR callbacks are placed in the macrotask queue.The confusion arises from the distinction between the initial request setup and the resolution of promises. Here’s a detailed breakdown:
Fetch Request:
fetch request, the JavaScript runtime handles the request asynchronously. The act of making the network request itself doesn’t fall into the microtask or macrotask queue because it’s handled by the browser’s Web API.Fetch Response Handling:
.then callbacks) falls under microtasks. This means the callback functions attached to the fetch promise are processed in the microtask queue..then callbacks): Microtasks.While the fetch function itself initiates a network request that is handled asynchronously by the browser, the promise it returns is resolved as a microtask. This means that once the network request completes, the promise resolution handlers (.then, .catch, etc.) are placed in the microtask queue, ensuring they are processed before any pending macrotasks.