DEV Community

Cover image for Event-driven architecture: navigating the single threaded nature of Node.js
Joshua Ng'ang'a Raphael
Joshua Ng'ang'a Raphael

Posted on • Updated on

Event-driven architecture: navigating the single threaded nature of Node.js

JavaScript's 'comeback story'
Despite its popularity nowadays, JavaScript had been once relegated to a simple scripting language primarily for client-side web development.
Over the years, JavaScript has however experienced a resurgence, becoming a versatile and powerful language suitable for use across both the front and back-end in software development.
One major factor to this resurgence is the development of Node.js which allowed for the execution of JavaScript on the server-side, thus giving rise to the full-stack JavaScript developer.


Lightweight and Efficient

Node.js is a lightweight runtime environment which is built on the JavaScript V8 engine which allows for the rendering of JavaScript server-side. It is a lightweight, efficient, high performing environment which has allowed for the development of scalable and high-performance software such as

  • real-time applications
  • web servers
  • web applications
  • Command Line tools

Node.js was written to provide JavaScript functionality on the back and since JavaScript is a single-threaded language, Node.js was developed using a single-threaded model.
This simply means that Node.js has a single event loop that executes only one task at a time, be it processing incoming requests, handling callbacks, and executing JavaScript code.
Node.js however implements a Non-blocking I/O (input/output) model to bypass whatever bottlenecks that may arise in single-threaded execution.
This allows for input/output operations to be executed asynchronously allowing for the handling of a large number of concurrent connections which allows for the development of scalable applications.
The implementation of an event-driven non-blocking I/O model helps to eliminate a few performance issues one may experience using single-threaded systems. However one may still encounter some limitations which we discuss as well as some workarounds to them.


Blocking Operations: Any synchronous or blocking operations can be encountered thus blocking an application's responsiveness and impacting performance
In Node.js, synchronous methods may be found in either native modules; which are modules written outside as well as in the Node.js standard library. For example in the File System module, an attempt to read from the file system may be written as follows

const fs = require('node:fs');
const data = fs.readFileSync('/file.md'); // blocks here until file is read. Synchronous methods end with the word Sync 
console.log(data);
moreWork(); // will run after console.log
Enter fullscreen mode Exit fullscreen mode

Use of such synchronous methods may exhibit poor performance.
Node.js however provides alternative asynchronous methods to each synchronous blocking method.
Our previous example can thus be rewritten to provide asynchronous, non-blocking execution as follows;

const fs = require('node:fs');
fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
});
Enter fullscreen mode Exit fullscreen mode

Sometimes it is however not possible to use the non-blocking alternative of a synchronous method.
One vital tool Node.js provides for this scenario is Libuv which is an open-source library that is the core behind Node.js's event-driven, non-blocking model.


Concurrency Limitations - Node.js is built on a single loop, non-blocking event-driven system that allows it to manage I/O-intensive operations.

Event loop visual

It is however a single-threaded system that uses one event loop. This causes serious issues when executing CPU-intensive tasks as applications are blocked from further execution. Node.js however provides an ingenious solution in 'worker threads'.
Worker threads, introduced in v10.5 of Node.js provide for the offloading of CPU-intensive tasks out from the event loop allowing for the execution of threads in parallel in a non-blocking manner. They essentially provided a separate runtime for CPU-intensive tasks by offloading them from the main event loop and thus maintain application responsiveness. They are however used at a cost.

Threading issues
Whilst a step forward towards achieving true parallelism using Node.js, the use of threads is not without a few challenges;

  • Increased complexity; one must now manage the synchronization between the primary and worker threads.
  • Restricted APIs; worker threads are only granted access to a restricted set of APIs not including DOM and UI APIs.
  • Significant resource allocation; each thread is assigned its own instance of the V8 JavaScript engine.

Although solving the issue of limited concurrency, one must use worker threads conservatively and only when necessary.


Error handling
The asynchronous nature of Node.js model which makes use of callback-based APIs may lead to 'callback hell' as one may have to read through large amounts of code to identify the specific callback that is experiencing an error.
Uncaught exceptions might also terminate the Node.js process abruptly and can crash the application if not handled properly.
Stack traces which provide information on the sequence of function calls may not accurately provide a solution to finding the source of error as they are asynchronous in nature
Error handling and debugging in Node.js should be done using provided debugging tools and libraries.
Consistent error handling mechanisms such as use of the 'throw' as well as the 'try...catch'
also provide for easier debugging.

Still the preferred solution

This article outlines a few factors one should consider when choosing to work with Node.js. Despite some limitations, workarounds have been provided by the brilliant contributors and developers that make Node.js a reality.
For scalable, efficient, high performing web-apps and real-time applications, Node.js is still a strong contender for the preferred environment to work with.

Top comments (0)