Introduction
Node.js is single-threaded, but it efficiently handles concurrency using its event-driven, non-blocking architecture. The event loop is at the heart of this mechanism, managing asynchronous operations such as I/O tasks, timers, and callbacks.
In this blog, we will break down:
- The Call Stack
- The Web APIs
- The Microtask Queue
- The Macro-task Queue (Callback Queue)
- The Event Loop
By the end, you will understand how Node.js executes JavaScript asynchronously and why some callbacks execute before others.
1. Call Stack
The Call Stack follows the Last In, First Out (LIFO) principle and is responsible for executing JavaScript functions.
Example:
function greet() {
console.log("Hello, World!");
}
greet();
Execution Flow:
-
greet()
is pushed onto the call stack. -
console.log()
insidegreet()
is pushed onto the stack. - "Hello, World!" is printed.
-
console.log()
is popped off. -
greet()
is popped off.
When the stack is empty, Node.js can execute asynchronous tasks from the queues.
2. Web APIs (Node.js APIs)
Asynchronous operations such as setTimeout, I/O tasks, and HTTP requests are delegated to Web APIs in the browser or Node.js APIs in the runtime.
Example:
console.log("Start");
setTimeout(() => {
console.log("Timeout Callback");
}, 0);
console.log("End");
Execution Flow:
- "Start" is logged.
-
setTimeout()
registers a callback in Web APIs. - "End" is logged.
- The Event Loop moves the
setTimeout()
callback to the Callback Queue and executes it after the call stack is empty.
Output:
Start
End
Timeout Callback
Even with 0ms
, the timer callback executes after synchronous code because it is moved to the Callback Queue.
3. Micro-task Queue
The Micro-task Queue (or Job Queue) has higher priority than the Callback Queue (Macro-task Queue).
Includes:
- Promises (
.then()
,.catch()
,.finally()
) - process.nextTick() (Node.js only, even higher priority than Promises)
Example:
console.log("Start");
setTimeout(() => console.log("setTimeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("process.nextTick"));
console.log("End");
Execution Flow:
- "Start" is logged.
-
setTimeout()
moves to the Callback Queue. -
Promise.then()
moves to the Micro-task Queue. -
process.nextTick()
moves to the Micro-task Queue (higher priority than Promises). - "End" is logged.
-
Execute Microtasks first:
- "process.nextTick" is printed.
- "Promise" is printed.
-
Execute Macro-tasks:
- "setTimeout" is printed.
Output:
Start
End
process.nextTick
Promise
setTimeout
4. Macro-task Queue (Callback Queue)
The Macro-task Queue stores callbacks from:
- setTimeout()
- setInterval()
- setImmediate()
- I/O tasks (fs.readFile, network requests, database queries)
- MessageChannel callbacks
Example:
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
Depending on the execution context, either setTimeout or setImmediate might execute first.
5. The Event Loop: Execution Order
The Event Loop follows this order:
- Execute all synchronous code (Call Stack)
- Execute process.nextTick()
- Execute Microtasks (Promises, async/await)
- Execute Macro-tasks (setTimeout, I/O, setImmediate)
- Repeat the cycle
Complete Example:
console.log("Start");
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("process.nextTick"));
console.log("End");
Execution Order:
- "Start" is printed.
-
setTimeout()
moves to Macro-task Queue. -
setImmediate()
moves to Macro-task Queue. -
Promise.then()
moves to Micro-task Queue. -
process.nextTick()
moves to Micro-task Queue (higher priority than Promises). - "End" is printed.
-
Execute Microtasks first:
- "process.nextTick" is printed.
- "Promise" is printed.
-
Execute Macro-tasks:
- "setTimeout" is printed.
- "setImmediate" is printed.
Final Output:
Start
End
process.nextTick
Promise
setTimeout
setImmediate
6. Summary
Micro-tasks (Higher Priority)
✅ process.nextTick() (Node.js-only, highest priority)
✅ Promises (.then()
, .catch()
, .finally()
)
✅ MutationObservers (Browser-only)
Macro-tasks (Lower Priority)
✅ setTimeout(), setInterval()
✅ setImmediate() (executes after I/O tasks)
✅ I/O operations (fs.readFile, network requests)
✅ MessageChannel callbacks
Execution Order:
- Synchronous Code (Call Stack)
- process.nextTick()
- Promises (Microtasks)
- Macro-tasks (setTimeout, setInterval, setImmediate, I/O tasks)
7. Conclusion
The Event Loop is the backbone of Node.js' asynchronous execution. Understanding it helps in:
- Writing efficient, non-blocking code.
- Avoiding callback hell.
- Optimizing performance in high-concurrency applications.
Next Steps:
✅ Try different combinations of async operations.
✅ Use Promises and async/await for cleaner code.
✅ Experiment with network and file system tasks in Node.js.
Would you like a visual representation or an interactive demo? Let me know!
Top comments (0)