Understanding the Event Loop in node.js

Understanding the Event Loop in node.js

The event loop can be called the heart of Node.js. It is used to handle asynchronous I/O tasks in Node.js. So how is it structured and how does it work? Let's find out in the following article.

What is the Event Loop?

The event loop allows Node.js to perform asynchronous I/O tasks, even though JavaScript is single-threaded by actually offloading operations to the operating system whenever possible.

Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes, the kernel notifies Node.js so that the attached callback function can be added to the poll queue and eventually executed.

How does the Event Loop work?

When Node.js starts, it initializes the Event Loop, processes any input script that it has been provided (or an REPL), which may include performing some asynchronous functions, schedulling timers, or process.nextTick(), and after that starts processing the Event Loop.

The following diagram provides a high-level overview of the sequence of events in the Event Loop.

banner

Note: each block is considered as one "phase" of the event loop.

Each phase has a FIFO queue of callbacks. Each phase has a specific task, but generally, when the Event Loop steps into a specific phase, it will process any data for that phase and then execute the callbacks in the queue for that phase until it is empty or reaches the execution limit. Then the Event Loop moves on to the next phase.

Since each phase can have a large number of callbacks waiting to be processed, some of the callbacks for timers may have a longer wait time than the initial threshold set, as the initial time threshold only guarantees the shortest wait time, not the exact waiting time.

For example,

setTimeout(() => console.log('hello world'), 1000);

The 1000ms is the shortest waiting time, but it doesn't mean that the console.log statement will be executed exactly after 1000ms.

Overview of Event Loop phases

  • Timers: Executes callbacks scheduled by setTimeout() and setInterval().
  • Pending callbacks: Executes I/O callbacks deferred to the next loop iteration.
  • Idle, prepare: Used for internal purposes by Node.js.
  • Poll: Retrieves new I/O events, executes related callbacks (almost all with the exception of close callbacks, timer callbacks, and setImmediate()).
  • Check: Executes setImmediate() callbacks.
  • Close callbacks: Executes close callbacks, e.g. socket.on("close").

Between each iteration of the Event Loop, Node.js checks if it is waiting for any asynchronous I/O or timers and exit if there is none.

Detailed Event Loop phases

Timers

A timer specifies a threshold after which a callback can be executed. The timer callbacks will run as soon as possible after the specified amount of time has passed. However, they can also be delayed for some time.

Note: Technically, poll controls when the timers are executed.

For example, let's say we set up a setTimeout() that executes after 100ms and then run an someAsyncOperation function that asynchronously reads a file and takes 95ms to complete:

const fs = require('fs');

function someAsyncOperation(callback) {
  // assume reading a file takes 95ms
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms`);
}, 100);

// someAsyncOperation takes 95ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // event loop will be delayed by 10ms...  
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

When the Event Loop steps into the poll phase and there are no callbacks for timers, one of the two things happens:

  • If the poll queue is not empty, the Event Loop will iterate through its callbacks and execute them one by one until the queue is empty or it reaches the system-specified limit.
  • If the poll queue is empty, one of the two things happens:
  • If there are setImmediate() callbacks scheduled, the Event Loop will exit the poll phase and proceed to the check phase to execute those scheduled callbacks.
  • If there are no setImmediate() callbacks scheduled, the Event Loop will wait for callbacks to be added to the queue and then execute them immediately.

When the poll queue is empty, the Event Loop checks if any timers have reached their threshold for execution. If one or more timers are ready, the Event Loop will go back to the timers phase to execute those callbacks.

Pending callbacks

This phase executes callback functions for some system operations, such as certain types of TCP errors. For example, if a TCP socket receives ECONNREFUSED when trying to connect, some *nix systems want to wait to report the error. It will be queued here to await its turn.

Poll

The poll phase has two main functions:

  • Calculate how long it should block and poll for I/O events, then:
  • Handle events from the poll queue

When the Event Loop steps into the poll phase and there are no timer callbacks, one of two things will happen:

  • If the poll queue is not empty, the Event Loop will iterate through its callbacks and execute them one by one until the queue is empty or it hits the limit of the system.
  • If the poll queue is empty, one of two things will happen:
  • If there are scheduled commands by setImmediate(), the Event Loop will exit the poll phase and proceed to the check phase to execute those scheduled commands.
  • If there are no commands scheduled by setImmediate(), the Event Loop waits for callbacks to be added to the queue and then executes them immediately.

When the poll queue is empty and there are no setImmediate() callbacks scheduled, the Event Loop checks if any timers have reached their execution threshold. If one or more have, the Event Loop will go back to the timers phase to execute those callbacks.

Check

This phase allows us to execute callbacks immediately after the poll phase completes. If the poll phase is idle and there are setImmediate() callbacks scheduled, the Event Loop can proceed to this phase instead of waiting for poll events.

setImmediate() is a special timer that runs in a separate phase of the Event Loop. It uses libuv API to schedule the execution of callbacks after the poll phase completes.

In general, once the code is executed, the Event Loop eventually reaches the poll phase - where it will wait for incoming connections, requests, etc… However, if a callback is scheduled by setImmediate() and the poll phase is in an idle state, it will end and continue to the check phase instead of waiting for the poll events.

Close callback

If a socket or handle is closed unexpectedly (e.g., socket.destroy()), then the 'close' event will be emitted in this phase. Otherwise, it will be emitted via process.nextTick().

Summary

The Event Loop in Node.js is implemented using libuv and consists of 6 phases, each handling a separate part of the work. Knowing this, we can explain the priority order of executing callbacks for functions like setTimeout, setImmediate, or process.nextTick. Regarding the priority order and the benefits of each, I will address this in another article. See you later!

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.
Author

Hello, my name is Hoai - a developer who tells stories through writing ✍️ and creating products 🚀. With many years of programming experience, I have contributed to various products that bring value to users at my workplace as well as to myself. My hobbies include reading, writing, and researching... I created this blog with the mission of delivering quality articles to the readers of 2coffee.dev.Follow me through these channels LinkedIn, Facebook, Instagram, Telegram.

Did you find this article helpful?
NoYes

Comments (0)