Node.js Event Loop: A Comprehensive Guide
Tags: webdev, javascript, node, eventdriven
Published: 2023-11-18 17:11:17

Node.js's asynchronous nature and event-driven architecture often lead to questions about its underlying mechanism, particularly the event loop. Understanding how Node.js manages tasks and handles asynchronous operations empowers developers to write efficient and responsive applications.

### Understanding the Event Loop

The event loop in Node.js enables it to do many things at once, even though JavaScript runs on a single thread. It does this by handing off tasks to the computer's core operating system, which can handle multiple tasks simultaneously. When one of these tasks finishes, Node.js gets a signal from the system, allowing it to run the associated callback with that task.

When Node.js starts, it initializes the event loop, processes the provided input script which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.

The event loop is the heart of Node.js, managing the execution of asynchronous operations in a single-threaded environment. It operates by continuously checking a series of phases to handle different tasks efficiently.

**The Event Loop Phases:**

1. **Timers**: Handles callbacks scheduled via setTimeout() or setInterval().

2. **Pending Callbacks**: Manages specific system operation callbacks deferred to the next loop iteration.

3. **Idle & Prepare**: Only used internally.

4. **Poll**: Retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); node will block here when appropriate.

5. **Check**: Executes setImmediate() callbacks after the poll phase ends.

5. **Close Callbacks**: Deals with callbacks related to closed handles or sockets, like the 'close' event.

Between each loop iteration, Node.js checks for pending asynchronous I/O or timers to ensure a clean shutdown if none exist.

#### 1. Timers Phase:

With the Timers phase, we provide the threshold time after which the associated callback runs.

For ex:

```
const fs = require('fs');

function asyncFunction(callback) {
  // Let's assume this 'fs.readFile' takes 90ms to read the whole 'abc.txt' file here.
  fs.readFile("abc.txt", callback);
}

const startTime = Date.now();

setTimeout(() => {
  const setTimeoutExecutionDuration = Date.now() - startTime;
  console.log(`${setTimeoutExecutionDuration}ms has passed since I was scheduled.`);
}, 100);

asyncFunction(() => {
 // Let's assume it takes 20ms
})
```

Now as we can see above, the timer that we have specified for the 'setTimeout' function is 100ms, and assuming the 'fs.readFile' called inside 'asyncFunction' takes 90ms and its callback takes 20ms to run.

Even though the 'setTimeout' callback has been scheduled to be executed after 100ms it will run after 110ms.

**Explanation:** In the above, scenario event loop will first enter the **Timers** phase will execute the **setTimeout** function, and will go into the **Poll phase**. In the pole phase, it will wait for other callbacks to be queued upon, as soon as fs.readFile completes its execution after 90ms, the event loop will put its callback into the poll queue and execute which takes 20ms. Now, since there are no other callbacks left, it will go into the **Timers queue** and execute the **setTimeout** callback. As we can see, the **setTimeout** has taken total of 110ms to run instead of its actual duration which was 100ms.

#### 2. Pending Phase:

Handles specific system operation callbacks, like TCP errors.

#### 3. Poll Phase:

In the event loop, the poll phase handles I/O operations and queued events.

**Tasks in Poll Phase:**

- If no timers are set, and the poll queue isn't empty, the event loop processes queued callbacks one by one until it finishes all or reaches a system-defined limit.

- If the poll queue is empty:
 - If **setImmediate()** scripts are scheduled, the event loop moves to the check phase to run those scheduled scripts.
 - If no **setImmediate()** scripts are scheduled, the event loop waits for new callbacks to be added to the queue and executes them immediately.

**Transition after Poll Phase:**

Once the poll queue empties, the event loop checks for timers that have reached their set thresholds.

If any timers are ready, the event loop shifts back to the timers phase to execute their associated callbacks.

#### 4. Check Phase:

The check phase in the event loop runs callbacks right after the poll phase finishes its tasks. If the poll phase is inactive and there are pending scripts scheduled with **setImmediate()**, the event loop moves directly to the check phase instead of waiting.

Typically, as code executes, the event loop reaches the poll phase, awaiting incoming connections or requests. However, if a **setImmediate()** callback is in line and the poll phase pauses, the loop swiftly transitions to the check phase without waiting for new poll events.

#### 5. Close Phase:

If a socket or handle is closed abruptly **(e.g. socket.destroy())**, the 'close' event will be emitted in this phase.

---


#### Conclusion

Understanding the Node.js event loop is pivotal for writing scalable and performant applications. Leveraging its phases effectively can enhance code reliability and efficiency.

By diving into practical examples and insights, we demystified the event loop, shedding light on its functionality and offering best practices for optimal code execution in Node.js.

If you want to dive deeper into the intricacies of Node.js event loop, timers, and process.nextTick(), you can explore the official Node.js documentation on [Event Loop, Timers, and process.nextTick()](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick).

Photo by Pixabay: https://www.pexels.com/photo/ferris-wheel-in-city-315499/