Three Major Traps to Avoid When Building Enterprise Node.js Apps

September 18, 2020

As part of our current series on Node.js and as a follow-on from Five Enterprise Apps Using Node.js Under the Hood, in this post, we look at three major traps when building enterprise Node.js apps and workarounds for how to avoid them.

As we’ve seen, many enterprise-level organizations - from NASA to Netflix - are using Node.js to build their applications. While there are many advantages to using Node.js, there are some pitfalls that developers can fall foul of while developing their applications. Let’s take a look at three major ones and some of their workarounds.

1. Bugs introduced by dynamic typing

In programming languages, type systems are rules used to manage data in variables, expressions, and functions.

Dynamically typed languages check the type of the data when the program is running. Statically typed languages check data types when the program is being compiled. Dynamically typed languages have several advantages over statically typed languages, such as:

  • They are more concise: shorter code is quicker to write, read and maintain;
  • There are more possibilities for interactivity: dynamic typing is a good fit for interactive, REP-esque programming for rapid prototyping, real-time debugging of running program instances and live coding;
  • Test cases can catch runtime errors if you have a good test suite; Dynamic languages encourage a prototype-based approach, for instance in JavaScript and Node.js.

That said, dynamically typed languages can easily introduce bugs due to a lack of type checking.

How to avoid this?

TypeScript is an open source language that is a superset of JavaScript. It has various useful attributes:

  • Its main innovation is that it adds static typing to JavaScript, while being transcompilable to JavaScript.
  • It lets you describe the shape of an object, providing superior documentation and the opportunity to have TypeScript validate that your code will run correctly before you run it.
  • TypeScript code is changed into JavaScript code via the TypeScript compiler or Babel. The resulting code is clean and simple and runs anywhere JavaScript runs: in a browser, your apps or on Node.js.
  • As well as preventing bugs, TypeScript can improve the code editing process when used with a code editor with TypeScript integration.

2. Challenges with callbacks

Callbacks are fundamental to JavaScript’s design. In web browsers, events are taken care of by passing references to functions that act like callbacks. In Node.js, callbacks, until recently, have been the only way that asynchronous elements of your code could communicate with one another. They continue to be heavily used – for instance, package developers design their APIs around callbacks.

Some easy mistakes to make when using callbacks include:

Invoking a callback more than once.

How to avoid it: One way is to add a return keyword before every callback invocation.

Deeply nesting callbacks

Keeping a number of queued tasks in the background, each with its own callback, is known as “callback hell” and can lead to the code becoming difficult to understand and laborious to maintain.

How to avoid it:

  • Use a utility Node.js package, such as Async.js, that handles asynchronous JS patterns.
  • Declare these tasks as small functions and link them together.

Expecting callbacks to run synchronously

In other programming languages, we are used to two statements executing one after the other, except when there is an instruction to jump between statements. In JavaScript and Node.js, with callbacks, a specific function tends not to run well until the task it is waiting on is complete.

How to avoid it: Anything that needs to take place after a callback has fired should be invoked from within it.

Most of the issues that spring from nested callback functions can be overcome with the use of promises in Node.js. A promise is an enhancement to callback functions in Node.js. A promise can help alleviate the problems associated with nesting multiple callback functions together. The essential concept of a promise is the return value. Promises can be nested within one another to improve the look of code and make it easier to maintain, particularly when one asynchronous function is called after another.

3. Performance bottlenecks due to overuse of CPU-bound tasks

The biggest challenge developers face with Node.js is its inability to quickly process CPU bound tasks. When Node.js receives a CPU bound task, it sets all the CPU available to process it first, then answer other queued requests. This leads to slow processing and a delay in the event loop. As Node.js processes tasks asynchronously, it executes JS code on its single thread on an event basis, which can lead to an event loop.

How to avoid it?

Updates to Node.js have improved this issue in the last few years. In 2018, multithreading was introduced as an experimental feature with the 10.5.0 update. A new module called Worker threads enables the use of threads that execute JS in parallel. They are intended to help with performance of CPU-intensive JS operations. However, note that Worker threads can only be performed on machines with multiple cores since Node.js allows you to use one core per thread. Heavy parallel processes can be run on a different thread; however, this feature is still experimental in Node.js v12 (now on v14).

While Node.js is a relatively easy runtime for newcomers to get started on, there are some surprising gotchas. Fortunately, as we have seen, there are workarounds for the major traps and pitfalls.