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.