📜 ⬆️ ⬇️

10 major mistakes when developing on Node.js



Since the advent of Node.js, they both criticize and extol it. Disputes about the advantages and disadvantages of this tool do not subside and probably will not subside in the near future. However, we often lose sight of the fact that criticism of any language or platform is based on the problems that arise, depending on how we use these platforms. Regardless of how much Node.js complicates writing safe code and facilitates its parallelization, the platform has been around for quite some time, and a huge number of reliable and complex web services have been created on it. All of them are well scaled and have proved their stability in practice.

But, like any platform, Node.js is not immune from the mistakes of the developers themselves. In some cases, performance drops, in others - the system becomes almost unusable. And in this post I would like to consider the 10 most frequent mistakes that developers make with insufficient experience with Node.js.

Error one: 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 by asynchronous processing of I / O operations. For example, a Node.js query to a database for a document allows Node.js to pay attention to another part of the application:
')
//       .    Node.js      .. db.User.get(userId, function(err, user) { // ..   ,        }) 



However, a piece of processor-occupying code can block a cycle of events, forcing thousands of connected clients to wait for their execution to complete. An example of such code is an attempt to sort a very large array:

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

A call to the sortUsersByAge function is unlikely to create problems in the case of a small array. But when working with a large array, this will drastically reduce overall performance. There may be no problems if this operation is urgently needed, and you are sure that no one else expects a cycle of events (say, if you are doing a tool launched from the command line, and no asynchronous execution is needed). But for the Node.js server that serves thousands of clients at the same time, this approach is unacceptable. If this array of users is retrieved directly from the database, then the best solution would be to extract it already sorted. If the event cycle is blocked by the calculation cycle of the total result of a large number of financial transactions, then this work can be delegated to some external executor so as not to block the event cycle.

Unfortunately, there is no silver bullet for solving problems of this type, and in each case an individual approach is needed. The main thing is not to overload the processor as part of the execution of the Node.js instance, which works in parallel with several clients.

Error two: callback call more than once


The work of JavaScript is based on callbacks. In browsers, events are handled by passing links to functions (often anonymous) that act as callbacks. Previously, in Node.js, callbacks were the only way to connect the asynchronous parts of the code to each other, until promises were introduced. However, callbacks are still in use, and many package developers still access them when designing their APIs. A common mistake is calling the callback more than once. Usually a method that does something asynchronously, expects the last argument of a function that it will call after completing its asynchronous task:

 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) }) } 

Notice the return statement after each done call, except for the last one. The fact is that calling a callback does not interrupt the execution of the current function. If you comment out the first “return”, passing a password to this function that is not a string will result in a call to “computeHash”. And depending on the further scenario of the work “computeHash”, “done” can be called repeatedly. Any unauthorized user who has used this feature may be taken by surprise by calling the callback several times.

To avoid this error, it is enough to be vigilant. Some developers have made it a rule to add the “return” keyword before each callback call:

 if(err) { return done(err) } 

In many asynchronous functions, the return value is not important, so this approach often avoids a multiple callback.

Error Three: deeply nested callbacks


This problem is often called “Callback Hell”. Although this in itself is not a mistake, it can cause the code to quickly get out of control:

 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') }) }) }) } 



The more complex the task, 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. At the same time, many people think that it is best to use modules that implement asynchronous JavaScript patterns, such as 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() { // ... }) } 

In addition to “async.waterfall”, Async.js contains a number of other functions that provide asynchronous execution of JavaScript. For brevity, a rather simple example is presented here, but in reality, often, everything is much worse.

Error four: expect that callbacks will be executed synchronously


Asynchronous programming with callbacks is not unusual for JavaScript and Node.js. Other languages ​​have taught us the predictability of the order of execution, when two expressions are executed sequentially, one after another, if there are no special instructions for switching between them. But even in this case, we are often limited to conditional operators, cycles, and function calls.

However, in JavaScript, callbacks allow you to do so that a certain function may not be executed until some task is completed. Here the function will be executed without stopping:

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

When calling the “testTimeout” function, “Begin” will be displayed first, then “Waitng”, and after about a second - “Done!”. If something must be done after calling the callback, then it must be called in the callback itself.

Fifth error: assigning “exports” instead of “module.exports”


Node.js works with each file as with a small isolated module. Suppose your package contains two files a.js and b.js. In order for b.js to access the functionality from a.js, the latter must export this functionality by adding properties to the “exports” object:

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

If this is done, then any a.js request will return an object with the verifyPassword function in the properties:

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

And if we need to export this function directly, and not as a property of any object? We can do this by redefining the “exports” variable, but the main thing is not to access it as a global variable:

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

Notice the “exports” as a property of the “module” object. The difference between “module.exports” and “exports” is very large, and misunderstanding of this leads to difficulties for beginner Node.js developers.

Error Six: generation of errors inside callbacks


In JavaScript, there is such a thing as an exception. Imitating the syntax of almost all traditional programming languages ​​that also have exception handling, JavaScript can generate and catch exceptions using try-catch blocks:

 function slugifyUsername(username) { if(typeof username === 'string') { throw new TypeError('expected a string username, got '+(typeof username)) } // ... } try { var usernameSlug = slugifyUsername(username) } catch(e) { console.log('Oh no!') } 

However, in asynchronous execution cases, try-catch will not work as expected. For example, if using a large try-catch block you try to protect an impressive piece of code with multiple asynchronous segments, this may not work:

 try { db.User.get(userId, function(err, user) { if(err) { throw err } // ... usernameSlug = slugifyUsername(user.username) // ... }) } catch(e) { console.log('Oh no!') } 

If the callback passed to db.User.get is called asynchronously, the try-catch block will not be able to catch the errors generated by the callback, since it will be executed in a different context than the try-catch context. Errors in Node.js can be processed differently, but you need to stick to the same template for the arguments of all function callbacks (err, ...) - the first argument in each callback is to wait for an error, if one happens.

Error Seven: assume that all numbers are integers


In JavaScript, there is no integer data type, here all numbers are floating point. You may find that this is not a problem, as it is not often that numbers are large enough to cause problems due to floating point restrictions. It's a delusion. Since floating-point numbers may contain integer representations only up to a certain value, exceeding it at any calculation immediately leads to problems. Strangely enough, this expression in Node.js is regarded as true:

 Math.pow(2, 53)+1 === Math.pow(2, 53) 

The oddities with numbers in javascript don't end there. In spite of the fact that these are numbers with a floating point, operators working with integer data work with them:

 5 % 2 === 1 // true 5 >> 1 === 2 // true 

However, unlike arithmetic, bitwise operators and shift operators work only with the last 32 bits of such large “integers”. For example, if you shift “Math.pow (2, 53)” by 1, then the result will always be 0. If you apply the bitwise OR, it will also be 0.

 Math.pow(2, 53) / 2 === Math.pow(2, 52) // true Math.pow(2, 53) >> 1 === 0 // true Math.pow(2, 53) | 1 === 0 // true 

Most likely, you rarely encounter large numbers, but when this happens, use one of the many libraries that perform exact mathematical operations with large numbers. For example, node-bigint .

Error eight: ignoring the benefits of streaming APIs


Suppose you need to create a small proxy server that processes responses when you request any data from another server. Say for working with images with Gravatar:

 var http = require('http') var crypto = require('crypto') http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } var buf = new Buffer(1024*1024) http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { var size = 0 resp.on('data', function(chunk) { chunk.copy(buf, size) size += chunk.length }) .on('end', function() { res.write(buf.slice(0, size)) res.end() }) }) }) .listen(8080) 

In this example, we take an image from Gravatar, read it in Buffer and send it as an answer to the request. Not the worst scheme, because these images are small. And if you need to proxy gigabyte content? It is better to use this method:

 http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { resp.pipe(res) }) }) .listen(8080) 

Here we take the image and simply broadcast as a response to the client, without reading the entire buffer.

Error Nine: Using Console.log for debugging


Console.log allows you to display anything on the console. Pass the object to it, and it will output a JavaScript object literal to the console. Console.log takes 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 lines. It is better to use some library specially written for this, like debug . With the help of such libraries, you can easily enable and disable the debug mode when you start the application. For example, when using “debug”, if you do not set the corresponding environment variable DEBUG, then debugging information will not get into the terminal:

 // app.js var debug = require('debug')('app') debug('Hello, %s!', 'world') 

To enable debug mode, simply run this code by setting the DEBUG environment variable to “app” or “*”:

 DEBUG=app node app.js 

Error ten: not using dispatcher programs


Regardless of whether your code is executed in production or in your local environment, it is highly recommended to use a dispatcher. Many experienced developers believe that the code should “fall” quickly. If an unexpected error occurs, do not attempt to process it, let the program crash so that the dispatcher can restart 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 simplifies the development process on Node.js. The following dispatchers can advise:


Each of them has its pros and cons. Someone works well with multiple applications on the same machine at the same time, some are better at logging. But if you want to start using the dispatcher, then choose any of the suggested ones.

Conclusion


Some of the errors described can have a destructive effect on your program, some can cause frustrations when implementing the simplest things. Although Node.js is simple enough for a newbie to start working with him, there are many moments in which it's easy to screw up. If you are familiar with other programming languages, then some of these errors may be known to you. But these 10 errors are typical for beginner Node.js developers. Fortunately, they are pretty easy to avoid. I hope that this small article will help novice developers write stable and effective applications for all of us.

Source: https://habr.com/ru/post/255895/


All Articles