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.
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.
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.
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.
setTimeout()
and setInterval()
. setImmediate()
). setImmediate()
callbacks. 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.
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:
setImmediate()
callbacks scheduled, the Event Loop will exit the poll phase and proceed to the check phase to execute those scheduled callbacks. 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.
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.
The poll phase has two main functions:
When the Event Loop steps into the poll phase and there are no timer callbacks, one of two things will happen:
setImmediate()
, the Event Loop will exit the poll phase and proceed to the check phase to execute those scheduled commands. 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.
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.
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()
.
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!
5 profound lessons
Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!
Subscribe to receive new article notifications
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.
Comments (1)