Since the release of Node.js, many developers have honored it with high praise, with many harsh criticism. Such “holivars” will probably never stop. The important thing in these debates is that any platform and any programming language is criticized for certain weaknesses, which are always due to the way we use the given tool.

It is not easy to write “traditional” code on Node.js, but it is easy to write asynchronous code, the platform has been around for quite a long time, and a huge number of reliable, high-tech, easily scalable web services have been created on it, demonstrating stability in work throughout its existence …

But when working with any platform – and Node.js is no different – developers make mistakes. Some of these errors hinder performance, and some give the impression that Node.js is unusable for any purpose at all. Let’s take a look at 10 common beginner mistakes and how to avoid them.

ERROR 1: Blocking the event loop

JavaScript in Node.js (as in the browser) creates a single threaded environment. This means that no application component is running at the same time; instead, concurrency is achieved by processing I / O operations asynchronously. For example, after making a request to the database engine to retrieve a document, while waiting for a response, Node.js can work in parallel with another part of the application.

// By fetching the user object from the database, Node.js can execute other parts of the code from the moment this function starts executing
   db.User.get (userId, function ( err, user ) {
        // until the "user" object is loaded here
})
}

However, to block the event loop, all you need to do is add some processor-intensive code with a large number of connected clients – and all clients will wait in the queue. CPU performance problems are provided when trying to sort large arrays, running a very long loop, and stuff like that. Example:

function  sortUsersByAge ( users ) {
        users.sort ( function ( a, b ) {
                return a.age <b.age? -1 : 1
        })
}

Calling a function sortUsersByAgewith a small array of users will work just fine, but with a large number of users, the overall performance degradation can be dire. If it is absolutely necessary to do this and you are sure that nothing will hang in the queue (for example, you are writing some command line, and it’s okay if everything is executed synchronously), this problem is not terrible for you. But if you do something similar on a Node.js server with thousands of users, you are guaranteed blocking the thread.

If the user array is retrieved from the database, the correct solution would be to take it already sorted directly in the database. If the event loop was blocked by a loop that calculates the sum of a very long history of financial transaction data, it is better for this event to attach a separate handler / queue to release the lock. If a person has never dealt with such a fight with locks, then he may not understand the phrase “connect a separate handler / queue”, if you can give an example here or just concretize the proposed solution.

There are no ideal solutions in such situations, in each case everything is individual. The main point is not to perform high CPU operations on Node.js instances to which clients are connected in parallel.

ERROR 2: Calling the callback multiple times

Jacascript callbacks have been used since the Battle of Kulikovo. In web browsers, events are handled by passing (often anonymous) parameters to functions by reference, where functions behave like callbacks. Until recently, callbacks in Node.js were the only way for asynchronous code elements to communicate with each other – until promises appeared. But callbacks have not gone away, many package developers still build APIs on callbacks. And a typical mistake here is calling the callback several times. Typically, a function that does something asynchronously expects another function as its last argument to be called when the asynchronous operation has completed.

module .exports.verifyPassword = function ( user, password, done ) {
        if ( typeof password! == 'string') {
                done ( new  Error ('password should be a string'))
                return
        }
        computeHash (password, user.passwordHashOpts, function ( err, hash ) {
                if (err) {
                        done (err)
                        return
                }
                
                done ( null , hash === user.passwordHash)
        })
}

In the example, we see that an doneoperator is registered in each call return, except for the last call. If you comment out the first one return, passing a non-string password to this function will still call computeHash. Depending on how it computeHashworks in this case, the “done” callback may be called multiple times. For some, this will be an unpleasant surprise.

To avoid surprises, you just need to be careful. Some developers have returngotten into the habit of adding before each callback call:

if (err) {
        return done (err)
}

For many asynchronous functions, meaning returnis almost irrelevant, but this approach often avoids errors.

ERROR 3: Deep nesting of callbacks

This phenomenon, referred to on the Internet as “callback noodles,” by itself, does not necessarily lead to disaster. But a beginner can quickly lose control of their own code.

function  handleLogin ( ..., done ) {
        db.User.get (..., function ( ..., user ) {
                if (! user) {
                        return done ( null , 'failed to log in ')
                }
                utils.verifyPassword (..., function ( ..., okay ) {
                        if (okay) {
                                return done ( null , 'failed to log in ')
                        }
                        session.login (..., function () {
                                done ( null , 'logged in ')
                        })
                })
        })
}

And the more difficult the task at hand, the more convoluted the code – it is extremely difficult to read and maintain. One way to solve this problem is to break down tasks into micro-functions and connect them step by step. Although, the simplest (subjective) solution would be to use the Node.js batch utility for asynchronous Javascript templates – Async.js:

function  handleLogin ( done ) {
        async .waterfall ([
                function ( done ) {
                        db.User.get (..., done)
                },
                function ( user, done ) {
                        if (! user) {
                        return done ( null , 'failed to log in ')
                        }
                        utils.verifyPassword (..., function ( ..., okay ) {
                                done ( null , user, okay)
                        })
                },
                function ( user, okay, done ) {
                        if (okay) {
                                return done ( null , 'failed to log in ')
                        }
                        session.login (..., function () {
                                done ( null , 'logged in ')
                        })
                }
        ], function () {
                // ...
        })
}

Likewise async.waterfall, the Async.js module provides a number of other functions for working with asynchronous templates. For the sake of brevity, the examples in the article are not terrifying at all, in reality it can be much worse.

ERROR 4: Waiting for synchronous callbacks

Asynchronous programming with callbacks is inherent not only in Javascript and Node.js, but thanks to it, these tools are so popular. In many other programming languages, the order of execution is predictable, two expressions will execute one after the other unless otherwise noted – and in any case we are limited to conditionals, loop statements, and function calls.

But in Javascript, a bell function that is waiting for an operation to complete may not work as intended. In the example below, the function will run to the end, without stopping:

function  testTimeout () {
        console .log (“Begin”)
        setTimeout ( function () {
                console .log (“Done!”)
        }, duration * 1000 )
        console .log (“Waiting ..”)
}

The function call will testTimeout()first print “Begin”, then “Waiting ..” and only after a second the message “Done!”

Everything that should happen after calling a callback should be written inside it.

ERROR 5: exportsMixed use withmodule.exports

Every file in Node.js is a small, isolated module. If your project has two files, say, “a.js” and “b.js”, then in order for the file “b.js” to access the functionality of the file “a.js”, the values ​​of the latter must be exported. Let’s assign them to the object parameters exports:

// a.js
exports.verifyPassword = function ( user, password, done ) {...}
module .exports.verifyPassword = function ( ... )

As a result, upon request “a.js” we will get an object with a function verifyPassword.

// b.js
require ('a.js') // {verifyPassword: function (user, password, done) {...}}

But what if we need to export a function, not as an object value? To do this, you need to override exports, but as a local, not a global variable:

In fact, initially exports and module.exports always refer to the same object: var exports = module.exports = {}; – and our module, in fact, always returns exactly module.exports. But if we assign a exportsdifferent value to the variable in the process , it will no longer be referenced module.exports, and our module will not return anything. A simple recipe for possible confusion – always use one or the other; safer and more convenient – module.exports.

Leave a Reply

Your email address will not be published. Required fields are marked *