10 major mistakes when developing on Node.js
Ever since it was first introduced, Node.js has met with a mixed response, getting both criticism and extolment. Controversy over the advantages and disadvantages of this tool has lingered for several years and has yet to show sign of subsiding. What we often lose sight of, however, is the fact that most criticism we make against a language or a platform is largely based on how we use them. Regardless of how much Node.js complicates the writing of secure code and facilitates its parallelization, the platform has been around for quite some time, and it has created a huge number of reliable and complex web services, all of which have demonstrated scalability and sustainability.
But, like any other platform, Node.js is not immune to the mistakes of the developers. In some cases, performance drops, while the system becomes practically unusable in others. This post will discuss the 10 most common mistakes made by developers with insufficient experience with Node.js and how to fix them.
Mistake #1: Blocking the event loop
JavaScript in Node.js (as in the browser) provides a single-threaded environment. This means that two or more parts of your application cannot run simultaneously. Parallelization is carried out due to asynchronous processing of input/output operations. For example, querying Node.js for a database for a document allows Node.js to focus on another part of the application:
// Trying to retrieve user data from the database. From now on, Node.js is free to execute other parts of the code. db.User.get (userId, function (err, user) { | |
// .. until the user data is retrieved here | |
}) |
However, a piece of processor-occupying code can block the event loop, causing thousands of connected clients to wait for completion. An example of such code is an attempt to sort a very large array:
Calling the sortUsersByAge function is unlikely to cause problems in the case of a small array. But when working with a large array, this will catastrophically reduce overall performance. Problems may not arise if this operation is absolutely necessary, and you are sure that no one else expects an event loop (say, if you are making a tool that is launched from the command line, and asynchronous execution is not needed). But for a Node.js server serving thousands of clients at the same time, this approach is unacceptable. If this user array is retrieved directly from the database, then the best solution would be to retrieve it already sorted. If the cycle of events is blocked by the cycle of calculating the overall result of a large number of financial transactions, then this work can be delegated to some external executor so as not to block the cycle of events.
Unfortunately, there is no silver bullet to solve problems of this type, and each case requires an individual approach. The main thing is not to overload the processor as part of the execution of the Node.js instance, which simultaneously works with several clients.
Mistake #2: Calling a callback more than once
JavaScript is based on callbacks. In browsers, events are processed by passing links to functions (often anonymous) that act as callbacks. Earlier in Node.js, callbacks were the only way to link the asynchronous parts of the code with each other until promises were implemented. However, callbacks are still in use, and many package developers still use them when designing their APIs. A common mistake is to call a callback more than once. Usually, a method that does something asynchronously expects a function that it will call after the completion of its asynchronous task:
Note the return statement after each “done” call, with the exception of the last. The fact is that calling a callback does not interrupt the execution of the current function. If you comment out the first “return”, passing this function a password that is not a string will result in a call to “computeHash”. And depending on the further scenario of the operation “computeHash”, “done” can be called multiple times. Any unauthorized user using this function can be taken by surprise by calling a callback several times.
To avoid this error, just be vigilant. Some developers made it a rule to add the keyword “return” before each callback call:
In many asynchronous functions, the return value is not important, so this approach often avoids calling the callback multiple times.
Mistake #3: Deeply Nested Callbacks
This problem is often called the Callback Hell. Although this in itself is not an error, it can cause code to quickly get out of hand:
The more complex the task gets, the deeper the nesting can be. This leads to unstable and hard-to-read code that is difficult to maintain. One way to solve this problem is to separate each task into a separate function, and then link them together. At the same time, many people find it best to use modules that implement asynchronous JavaScript patterns, such as Async.js:
In addition to “async.waterfall”, Async.js also contains a number of other functions that enable asynchronous JavaScript execution. For the sake of brevity, a fairly simple example is presented here, but more often than not, everything is much worse in reality.
Mistake #4: Expecting that callbacks will be executed synchronously
Asynchronous callback programming is not unusual for JavaScript and Node.js. Other languages mightaccustom us to the predictability of the execution order, when two expressions are executed sequentially, one after another if there are no special instructions for moving between them. But even in this case, we are often limited to conditional statements, loops, and function calls.
However, in JavaScript, callbacks allow you to make sure that a certain function may not be executed until a certain task is completed. Here the function will execute without stopping:
When calling the “testTimeout” function, “Begin” will be displayed first, then “Waiting”, and after about a second – “Done!” If something needs to be done after calling the callback, then it must be called in the callback itself.
Mistake #5: Assigning “exports” instead of “module.exports”
Node.js treats each file as a small, isolated module. Let’s say your package contains two files a.js and b.js. In order for b.js to access functionality from a.js, the latter must export this functionality by adding properties to the “exports” object:
If this is done, then any a.js request will return an object with the “verifyPassword” function in the properties:
And what if we need to export this function directly, and not as a property of any object? We can do this by overriding the “exports” variable, but the main thing is not to access it as a global variable:
Pay attention to “exports” as a property of the “module” object. The difference between “module.exports” and “exports” is very large, and a misunderstanding of this leads to difficulties for beginner Node.js developers.
Mistake #6: Error generation inside callbacks
JavaScript has a concept like exception. Imitating the syntax of almost all traditional programming languages, which also have exception handling, JavaScript can generate and catch exceptions using try-catch blocks:
However, in cases of asynchronous execution, try-catch will not work as you expect. For example, if you try to protect an impressive piece of code with numerous asynchronous segments using a large try-catch block, then this may not work:
If the callback passed to db.User.get is called asynchronously, the try-catch block will not be able to intercept the errors generated in the callback because it will be executed in a different context than the try-catch context. Errors in Node.js can be handled in different ways, but you must adhere to one template for the arguments of all function (err, …) callbacks – the first argument in each callback is to expect an error if any.
Mistake #7: Assuming that all numbers are integers
JavaScript does not have an integer data type, here all numbers are floating-point numbers. You may think that this is not a problem since numbers that are not large enough to cause problems due to floating-point restrictions are not common. This is a delusion. Since floating-point numbers can contain integer representations only up to a certain value, exceeding it in any calculation immediately leads to problems. Oddly enough, this expression in Node.js is regarded as true:
The oddities with numbers in JavaScript don’t end there. Despite the fact that these are floating point numbers, they work with operators designed for integer data:
However, unlike arithmetic, bitwise operators and shift operators work only with the last 32 bits of such large “integer” ones. For example, if you shift “Math.pow (2, 53)” by 1, then the result will always be 0. If you apply bitwise OR, it will also be 0.
Most likely, you rarely encounter large numbers, but when this happens, use one of the many libraries that perform precise mathematical operations with large numbers. For example, node-bigint.
Mistake #8: Ignoring the benefits of streaming APIs
Suppose you need to create a small proxy server that processes responses when requesting any data from another server. Say, for working with images with Gravatar:
In this example, we take an image with Gravatar, read it in Buffer, and send it as a response to the request. Not a bad design, as these images are small. But what if you need to proxy gigabyte-sized content? It is better to use this method:
Here we take an image and simply transmit it as a response to the client, without reading the entire buffer.
Mistake #9: Using Console.log for debugging
Console.log allows you to output anything to the console. Pass it an object, and it will print a JavaScript object to the console. Console.log accepts any number of arguments and displays them, neatly separated by spaces. Many developers are happy to use this tool for debugging, but it is recommended not to use “console.log” in real code. Avoid “console.log” even in commented outlines. It’s better to use libraries specially written for this, like debug. Using these libraries, you can easily enable or disable debugging mode when starting the application. For example, when using “debug”, if you do not set the appropriate environment variable DEBUG, then the debug information will not get to the terminal:
To enable debug mode, just run this code by setting the DEBUG environment variable to “app” or “*”:
Mistake #10: Not using dispatcher programs
Regardless of whether your code runs in production or in your local environment, it is highly recommended that you use a dispatch program. Many experienced developers believe that code should “crash” quickly. If an unexpected error occurs, do not try to handle it, let the program crash so that the dispatcher restarts it within a few seconds. Of course, this is not all that dispatchers can do. For example, you can configure the restart of the program in case of changes in some files, and much more. This greatly facilitates the development process on Node.js. The following managers are recommended:
Each production process manager has its own pros and cons. Some work well with several applications on the same machine at the same time, while others are better at logging. But if you want to start using the dispatcher, then you may choose any of the proposed ones.
Discuss with our experts in developing on Node.js
Contact us
- Phone: +84 24 3202 9222
- Hotline: +84326752886
- Email: [email protected]
- Official Website: https://savvycomsoftware.com/