How the Node.js Event Loop Works

What is libuv?

  • libuv is a C library that provides asynchronous I/O and non-blocking operations.

  • It acts as a bridge between Node.js and the OS (Operating System).

  • It manages things like file system operations, networking, timers, child processes, and more.

The Event Loop in libuv

The event loop is at the heart of Node.js's asynchronous nature. It ensures that the JavaScript engine does not block on I/O operations.

  1. Event Loop Continuously Runs

    • The event loop keeps checking if the JavaScript engine’s call stack is empty.

    • If it's empty, it takes callbacks from the callback queue and pushes them into the call stack.

  2. Callback Queue

    • When Node.js hands over asynchronous tasks (e.g., file I/O, network requests) to libuv, it delegates them to the OS.

    • Once the task is completed, its corresponding callback function is placed in the callback queue.

    • The event loop then picks up this callback when the call stack is empty and executes it.

How the Event Loop Works Step by Step

1️⃣ process.nextTick() Phase (Microtask)

  • Before moving to any phase, the event loop first checks if there are any process.nextTick() callbacks.

  • If found, they are executed immediately before moving forward.

process.nextTick(() => {
  console.log("This runs before any I/O or timer callback.");
});

Priority: Always runs before any other task in the event loop.

2. Promise Callbacks / Other Microtasks

  • Next, the event loop checks the Microtask Queue, which contains:

  • Promise callbacks (.then(), .catch(), .finally())

  • If found, they execute before any other phase starts.

Promise.resolve().then(() => console.log("Promise callback executed"));

Priority: Runs right after process.nextTick(), before moving to timers or I/O

3. Timers Phase

  • The event loop checks if any setTimeout or setInterval timers are ready to execute.
setTimeout(() => {
  console.log("setTimeout executed");
}, 0);
  • Even with setTimeout(..., 0), it runs after microtasks.

Priority: Runs after all microtasks (process.nextTick and Promise callbacks).

Before moving to the Poll Phase, the event loop first checks if there are any process.nextTick() or Promise callbacks. These microtasks always run before moving to the next phase

4. I/O Polling Phase (Poll)

  • Here, Node.js checks for pending I/O tasks (like file reads, database queries).

  • If no timers are pending:

  • The event loop waits for I/O events.

  • If timers are pending:

  • It skips waiting and moves to the Check Phase.

Priority: Handles I/O tasks and determines if it should wait

Before moving to the check Phase, the event loop first checks if there are any process.nextTick() or Promise callbacks. These microtasks always run before moving to the next phase

5. Check Phase (setImmediate)

  • Callbacks from setImmediate() run in this phase.

  • setImmediate() runs after I/O but before closing callbacks.

setImmediate(() => console.log("setImmediate executed"));

Priority: Runs right after I/O in the Check Phase.

6. Close Callbacks Phase

  • Here, cleanup callbacks (like socket.on('close', fn)) are executed.

Priority: Runs just before the event loop iterates again.

🔁 Before Moving to the Next Cycle

Before going to the next cycle, the event loop checks again for:

  • process.nextTick()

  • Microtasks (Promise callbacks)

If there are any, it runs them before the next event loop iteration begins.

EXAMPLE

console.log("Start");

setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));

Promise.resolve().then(() => console.log("Promise resolved"));
process.nextTick(() => console.log("process.nextTick"));

console.log("End");

Output

Start
End
process.nextTick
Promise resolved
setTimeout
setImmediate

Example-2

setImmediate(() => console.log("setImmediate"));

setTimeout(() => console.log("Timer expired"), 0);

Promise.resolve().then(() => console.log("Promise"));

fs.readFile("./file.txt", "utf8", () => {
  setTimeout(() => console.log("2nd timer"), 0);

  process.nextTick(() => console.log("2nd nextTick"));

  setImmediate(() => console.log("2nd setImmediate"));

  console.log("File Reading CB");
});

process.nextTick(() => console.log("nextTick"));

console.log("Last line of the file.");

Output


Last line of the file.
nextTick
Promise
File Reading CB
2nd nextTick
2nd setImmediate
2nd timer
setImmediate
Timer expired

1. Synchronous Code Runs First

  • Call Stack executes synchronous statements first.

  • The first console.log("Last line of the file."); executes.

Output: Last line of the file.
  • Next, process.nextTick(() => console.log("nextTick")); is added to the Microtask Queue.

  • Promise.resolve().then(() => console.log("Promise")); is also added to the Microtask Queue.

2. Microtasks Execute Before Moving to Timers

  • Microtasks have higher priority than the Timer Phase, so they execute now.

  • process.nextTick(() => console.log("nextTick")); runs first.

Output: nextTick
  • Then, the Promise callback executes.
Output: Promise

3. fs.readFile() Starts Asynchronously

  • Since fs.readFile() is asynchronous, it moves to the I/O queue.

  • The event loop continues and reaches setTimeout() and setImmediate().

4. Timers and Check Phase in Main Event Loop

  • setTimeout(() => console.log("Timer expired"), 0);Timers Phase.

  • setImmediate(() => console.log("setImmediate"));Check Phase.

Now the Call Stack is empty, so the event loop moves to the next phase.

Once all tasks from a phase are completed, the event loop returns to the Poll Phase and waits for new tasks

Event Loop Behavior After Execution

🔹 What happens after all tasks are completed?

  • Once all timers, microtasks, and I/O callbacks are executed, the event loop returns to the Poll Phase.

  • If there are no pending I/O tasks, no setTimeouts, and no new events, the event loop enters an idle state.

  • It waits in the Poll Phase until new asynchronous tasks (I/O, timers, or events) arrive

5. I/O Callbacks Phase Executes

  • The file read operation completes and its callback (fs.readFile) is executed.

  • Inside the callback:

  • "File Reading CB" logs.

Output: File Reading CB
  • process.nextTick(() => console.log("2nd nextTick")); is added to Microtask Queue.

  • setImmediate(() => console.log("2nd setImmediate")); is added to Check Phase.

  • setTimeout(() => console.log("2nd timer"), 0); is added to Timers Phase.

6. Microtasks Run Before Moving to Next Phase

  • process.nextTick(() => console.log("2nd nextTick")); executes.
Output: 2nd nextTick

7. Check Phase Executes

  • Now, setImmediate(() => console.log("2nd setImmediate")); runs.
Output: 2nd setImmediate

8. Timers Phase Executes

  • setTimeout(() => console.log("2nd timer"), 0); runs.
Output: 2nd timer
  • Then, the first Check Phase setImmediate() runs.
Output: setImmediate
  • Finally, the Timers Phase executes the first setTimeout().
Output: Timer expired

Summary: How the Node.js Event Loop Works

The Node.js Event Loop is the heart of asynchronous execution, ensuring that JavaScript never blocks on I/O operations. Here’s a quick breakdown of how it works:

1. Synchronous code runs first on the call stack (e.g., console.log()).

  1. Microtasks (process.nextTick() and Promises) execute immediately after the synchronous code.
  2. Timers Phase handles setTimeout() and setInterval() callbacks.
  3. Poll Phase manages I/O operations like file reading and database queries.
  4. Check Phase executes setImmediate() callbacks before moving to the next event loop cycle.
  5. Close Callbacks Phase handles cleanup tasks (e.g., socket closures).
  6. Once all phases complete, the event loop returns to the Poll Phase and waits for new tasks.

Final Takeaways

1.Microtasks always execute before moving to the next event loop phase.
2.setImmediate() executes before setTimeout() if inside an I/O callback.
3.The event loop remains in the Poll Phase if no timers are scheduled.
4.If no pending async tasks exist, Node.js exits the process.

0
Subscribe to my newsletter

Read articles from Priyanshu Pandey directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Priyanshu Pandey
Priyanshu Pandey