NodeJs 09 : Libuv & Event Loop
Deep Dive into Node.js Event Loop Phases: Timers, Poll, Check, Close, process.nextTick, and Promise Callbacks
Node.js is known for its non-blocking, asynchronous nature, making it an excellent choice for building fast and scalable network applications. At the heart of this capability is the event loop, which is responsible for handling all asynchronous operations. In this blog, we’ll focus on the most critical phases of the event loop — timers, poll, check, close callbacks, process.nextTick, and promise callbacks — to understand how they work and how they interact with each other.
What is the Event Loop?
Before diving into the phases, let’s briefly recap what the event loop is. The event loop is a constantly running loop that processes and dispatches events or messages in a program. It’s the backbone of Node.js’s asynchronous programming model, allowing the runtime to perform non-blocking I/O operations even though JavaScript is single-threaded.
Key Event Loop Phases
The event loop is divided into several phases, each responsible for handling specific types of callbacks. The order of execution of these phases is crucial to understanding how Node.js processes asynchronous tasks.
1. Timers Phase
What it Does: The timers phase is responsible for executing callbacks scheduled by setTimeout() and setInterval(). These callbacks are added to a queue and executed when the specified time has elapsed.
How it Works?
- When you set a timeout or interval, the callback is registered with the event loop along with the delay.
- Once the delay has passed, the callback is moved to the timers queue to be executed in the next cycle of the event loop.
- Technical Details: The timers phase doesn’t guarantee exact timing. Instead, it checks whether the timer has expired and, if so, moves the callback to the queue for execution. This means the actual execution can be delayed due to the event loop’s current load.
2. Poll Phase
What it Does: The poll phase is one of the most critical phases. It is responsible for retrieving new I/O events, executing I/O-related callbacks, and handling other events such as reading from file descriptors.
How it Works?
- When the event loop enters the poll phase, it checks if there are any timers that need to be executed.
- If there are no timers, the event loop will block and wait for I/O events, hence the name “poll”.
- If there are pending I/O operations, their callbacks are executed during this phase.
- Technical Details: The poll phase is where the event loop might block, waiting for new I/O events. This blocking behavior is what allows Node.js to efficiently handle multiple I/O operations concurrently. The duration of the wait depends on the presence of timers and pending operations.
3. Check Phase
What it Does: The check phase is where callbacks scheduled by setImmediate() are executed. This phase runs after the poll phase.
How it Works?
setImmediate()is often used to execute a callback after the current poll phase has completed. It’s similar tosetTimeout()with a 0ms delay, but it is specifically tied to the event loop cycle.- Once the poll phase is complete, the event loop moves to the check phase and executes any
setImmediate()callbacks. - Technical Details:
setImmediate()is designed to execute callbacks immediately after the poll phase, ensuring that I/O operations are given priority. This makessetImmediate()useful for scheduling tasks that need to run as soon as possible but after I/O.
4. Close Callbacks Phase
What it Does: This phase is responsible for executing callbacks associated with the close event. This usually involves clean-up tasks after a stream or socket is closed.
How it Works?
- When an object like a stream or socket is closed, a
closeevent is emitted. - The corresponding callbacks for this event are executed in this phase.
- Technical Details: Close callbacks are essential for ensuring that resources are properly released and that any necessary clean-up is performed when a stream or socket is closed.
Microtasks: process.nextTick and Promise Callbacks
In addition to the standard event loop phases, Node.js also processes microtasks, which include process.nextTick and promise callbacks. These are executed immediately after the currently executing operation completes, before the event loop continues to the next phase.
1. process.nextTick
What it Does: process.nextTick() allows you to schedule a callback to be executed immediately after the current operation completes, but before the event loop moves on to the next phase.
How it Works?
- When you call
process.nextTick(), the callback is added to the next tick queue. - The event loop checks this queue before moving to the next phase and executes all callbacks in the queue.
- Technical Details:
process.nextTick()is not part of the event loop phases but is handled with a higher priority. This makes it useful for deferring execution within the same phase without waiting for the event loop to iterate.
2. Promise Callbacks
What it Does: Promise callbacks, including .then() and .catch(), are added to the microtask queue, similar to process.nextTick().
Get DEEPAK MAURYA’s stories in your inbox
Join Medium for free to get updates from this writer.
How it Works?
- When a promise is resolved or rejected, the associated callbacks are added to the microtask queue.
- The event loop processes these callbacks immediately after the current operation completes and before moving on to the next phase.
- Technical Details: Promise callbacks are part of the microtask queue, which has a higher priority than the standard event loop phases. This ensures that promise resolutions are handled as soon as possible, leading to more predictable asynchronous behavior.
Interaction Between Phases: Code Examples
- Let’s explore how these phases and microtasks interact using code examples:
Example 1: Basic Interaction Between Phases
const fs = require("fs");
const a = 10;
setImmediate(() => { console.log("setImmediate") });
fs.readFile('./file.txt', 'utf-8', (err, data) => {
console.log("file reading cb", data);
});
setTimeout(() => { console.log("timer expired") }, 0);
function printA() {
console.log("a = ", a);
}
printA();
console.log("last line of the file");// OUTPUT
a = 10
last line of the file
timer expired
setImmediate
file reading cb Hello from fileExplanation:
- Synchronous code (e.g.,
printA()andconsole.log("last line of the file")) is executed first. - Timers (with
setTimeout) are checked next and executed if their time has expired. - I/O operations (like
fs.readFile) are processed in the poll phase after all the timers have been handled. setImmediatecallbacks are executed in the check phase, which comes after the poll phase.
In this example, even though the timer had a 0 ms delay, it was executed before the setImmediate() callback because setTimeout is handled in the timers phase, which occurs before the check phase in the event loop. The file reading operation takes place in the poll phase, and its callback is executed after the
Example 2: Delayed Timer
const fs=require("fs");
const a=10;
setImmediate(()=>{console.log("setImmediate")});
fs.readFile('./file.txt','utf-8',(err,data)=>{
console.log("file reading cb",data);
})
setTimeout(()=>{console.log("timer expired")},9000);
function printA(){
console.log("a = ",a);
}
printA();
console.log("last line of the file");// OUTPUT
a = 10
last line of the file
setImmediate
file reading cb Hello from file
timer expiredExplanation:
- Synchronous code (
printA()andconsole.log("last line of the file")) is executed first. setImmediateis processed in the check phase, which happens after the poll phase.- I/O operations (like
fs.readFile) are handled in the poll phase. The callback associated withfs.readFileis executed as soon as the file reading completes. - Timers with a delay of
9000milliseconds are processed in the timers phase after the specified time has elapsed. Hence,"timer expired"is printed last.
The event loop executes synchronous code immediately, and asynchronous operations are handled in their respective phases. Since the timer is set with a longer delay, it executes only after all other callbacks have been handled. This explains why "timer expired" is the last message in the output.
Example 3: Adding Microtasks
const fs=require("fs");
const a=10;
setImmediate(()=>{console.log("setImmediate")});
Promise.resolve("promise").then(console.log);
fs.readFile('./file.txt','utf-8',(err,data)=>{
console.log("file reading cb ", data)
})
setTimeout(()=>{console.log("timer expired"),0});
process.nextTick(()=>{console.log("process.nextTick")})
function printA(){
console.log("A= ",a);
}
printA();
console.log("last line of file");// OUTPUT
A=10
last line of file
process.nextTick
promise
timer expired
setImmediate
file reading cb Hello from fileExplanation Summary
1. Synchronous code (printA() and console.log("last line of file")) runs first, resulting in "A= 10" and "last line of file" being printed immediately.
2. Microtasks:
process.nextTickhas the highest priority among all tasks and runs immediately after the synchronous code, printing"process.nextTick".- The resolved promise’s
.thencallback is placed in the microtask queue and runs next, printing"promise".
3. Timers Phase: The setTimeout callback with a 0 ms delay is executed in the timers phase, printing "timer expired".
4. Check Phase: The setImmediate callback runs in the check phase, printing "setImmediate".
5. Poll Phase: The fs.readFile callback is executed in the poll phase, printing "file reading cb Hello from file".
This order illustrates the priorities within the event loop, showing how microtasks (especially process.nextTick and promise callbacks) are executed before any other phase-related tasks.
Example 4: Complex Interaction of Microtasks and Event Loop Phases
const fs=require("fs");
setImmediate(()=>{console.log("setImmediate")});
setTimeout(()=>{console.log("Timer Expired"),0});
Promise.resolve("promise").then(console.log);
fs.readFile('./file.txt','utf-8',(err,data)=>{
setTimeout(()=>{console.log("2nd timer"),0});
process.nextTick(()=>{console.log("2nd nextTick")})
setImmediate(()=>{console.log("setImmediate")})
console.log("file reading cb ", data)
})
process.nextTick(()=>{console.log("nextTick")})
console.log("last line of file");// OUTPUT
last line of file
nextTick
promise
Timer Expired
setImmediate
file reading cb Hello from file
2nd nextTick
setImmediate
2nd timerExplanation:
1. Synchronous Code: "last line of file" is printed first.
2. Microtasks: process.nextTick has the highest priority, followed by the promise callback, resulting in "nextTick" and "promise" being printed next.
3. Timers Phase: The setTimeout callback with 0 ms delay is executed next, printing "Timer Expired".
4. Check Phase: The first setImmediate callback is executed, printing "setImmediate".
5. Poll Phase:
- The
fs.readFilecallback is executed, printing"file reading cb Hello from file". - Inside this callback, another
process.nextTickandsetImmediateare scheduled, which execute in their respective phases, printing"2nd nextTick"and"setImmediate"in that order.
6. Timers Phase (Next Iteration): Finally, the second setTimeout callback is executed, printing "2nd timer".
This output sequence demonstrates the priorities within the event loop phases, where process.nextTick and promise callbacks (microtasks) are executed before any other event loop phase tasks, followed by timers, I/O callbacks, and then setImmediate callbacks.







