The Event Loop in Node.js for JavaScript Developers
WARNING: this is a very technical article so be prepared to see a lot of jargon. You need to understand this stuff to become a better Node.js developer (or at least get the big picture).
I have been using Node,js for a few years now and sometimes I forget how this infamous Event Loop thing works. I have a good idea how the "normal" JavaScript Event Loop works in browsers (check my article on async JS code) but Node.js is not the browser environment.
Remember that your browser, let's say Chrome, uses the V8 JavaScript engine which is the most powerful and optimised JavaScript engine so far, but Node.js is based on the V8 engine with some added features due to its primary use which is server-side programming. Node.js execution model is based on JavaScript Event loop which is single threaded and based on JavaScript callback mechanism.
Because of the single-threaded design of Node.js environment, it is considered to be one of the most complicated architectures considering its performance in data-intensive processing. Being completely event-driven, Node.js is designed as an event-based platform, that is anything that occurs in Node.js is just a reaction to an event. Any operation done in Node.js passes through a series of callbacks (asynchronous by design aka non-blocking I/O).
The complete logic is abstracted from developers and is handled by a library called libuv (written in C++).
Libuv enforces an asynchronous, event-driven style of programming. Its core job is to provide an event loop and callback based notifications of I/O and other activities. libuv offers core utilities like timers, non-blocking networking support, asynchronous file system access, child processes and more.
It is essential to gain a thorough understanding of the Event Loop in in order to master Node.js, including how it works, common misconceptions, its various phases, and more.
The following are the most common myths about Node.js Event Loop and a short description on the actual workings:
- Misconception#1—Node.js Event Loop works in a different thread than the developer code: There are two threads maintained, one parent thread where the developer-related code or user-related operations run, and another where the event looping code runs. Any time an operation is executed, the parent thread passes over the work to the child thread, and once the child thread operation is completed, it pings the main thread to execute the callback:
- Fact: Node.js is single-threaded and everything runs inside the single thread. The Event Loop maintains the execution of the callback.
- Misconception#2—Thread pool handles asynchronous events: All asynchronous operations, such as callbacks to data returned by a database, reading filestream data, and WebSockets streams, are off loaded from a thread pool maintained by libuv:
- Fact: The libuv library does create a thread pool with four threads to pass on the asynchronous work (gathering data from database for example), but today's operating systems already provide such interfaces. So by principle, libuv will use those system asynchronous interfaces rather than the thread pool. The thread pool will only be used as the last resort.
- Misconception#3—Event Loop, like a CPU, maintains a stack or queue of operations: The Event Loop goes through a maintained queue of asynchronous tasks maintained via the FIFO rule (First In First Out), and executes the defined callbacks maintained in a queue:
- Fact: While there are queue-like structures involved in libuv, the callbacks are not processed through a stack. The Event Loop is more of a phase executioner with tasks processed in a round-robin manner.
Understanding Node.js Event Loop
Now that common misconceptions regarding Event Loop in Node.js have been set aside, let's look at the workings of the Event Loop in detail and all the phases in the Event Loop phase execution cycle. Node.js processes everything occurring in the environment in the following phases:
- Timers: This is the phase where all the setTimeout() and setInterval() callbacks are executed. This phase will run early because it has to be executed in the time interval specified in the calling functions. When the timer is scheduled, then as long as the timer is active the Node.js Event Loop will continue to run.
- I/O callbacks: Most common callbacks are executed here except timers, close connection events, setImmediate(). An I/O request can be blocking as well as non-blocking. It executes more things such as connection error, failed to connect to a database, and so on.
- Poll: This phase executes the scripts for timers when the threshold has elapsed. It processes events maintained in the poll queue (aka callback queue). If the poll queue is not empty, the Event Loop will iterate through the entire queue synchronously until the queue empties out or the system hard peak size is reached. If the poll queue is empty, the Event Loop continues with the next phase—it checks and executes those timers. If there are no timers, the poll queue is free; it waits for the next callback and executes it immediately.
- Check: When the poll phase is idle, the check phase is executed. Scripts that have been queued with setImmediate() will be executed now. setImmediate() is a special timer that has use of the libuv API and it schedules callbacks to be executed after the poll phase. It is designed in such a way that it executes after the poll phase.
Close callbacks: When any handle, socket, or connection is closed abruptly, the close event is emitted in this phase, such as socket.destroy(), connection close(), that is, all on (close) event callbacks are processed here. Not technically parts of the Event Loop, but two other major phases are nextTickQueue and other micro tasks queue. The nextTickQueue processes after the current operation gets completed, regardless of the phase of Event Loop. It is fired immediately, in the same phase it was called, and is independent from all phases.
To put it in perspective, the V8 engine inside Chrome browser monitors user's clicks and interactions whereas on the server the Event Loop in Node.js monitors for server events such as database query / response, network I/O, file I/O, etc.
All I/O operations are ;managed by the Event Loop without blocking other tasks.
Concurrency: Single Threaded and Non-Blocking I/O
In traditional programming paradigms, concurrency is handled via the use of multiple threads. Whenever an I/O request is received , a multi-threaded system spawns a new worker thread which handles the request. The spawn blocks the request until the response is returned. If all worker threads are busy, further requests will be denied.
Multi-threading is notoriously difficult because of the complexity it requires to manage multi-threads on the systems and making inter thread communication and other kinds of high performance computing. Very few developers achieve this kind of expertise cause most of this has been abstracted for simplicity sake (KISS...). So unless, you are doing computer science academic research (or build your own Node.js...), you most likely will never have to deal with this low level programming stuff.
This approach to concurrency is not the most efficient for heavy I/O. In contrast, Node.js uses the Event Loop system. The Event Loop monitors events. When we need to do I/O operations, a request is sent to the Event Loop, therefore while the response to an I/O operation is awaiting, the Event Loop uses the time in between for other tasks thereby keeping the process non-blocked and available for other constructs in the program to use. This way it achieves efficient concurrency in a single thread. When the response to an I/O operation is received, it goes back (callback) and executes the callback function that processes the data as desired.
I will not go into further details because it is complicated enough for my taste...and I still need to learn it myself.
Sources:
Typescript microservices
Node.js docs