Los 10 errores más comunes que cometen los desarrolladores de Node.js

Publicado: 2022-03-11

Desde el momento en que Node.js se presentó al mundo, ha recibido una buena cantidad de elogios y críticas. El debate aún continúa, y es posible que no termine pronto. Lo que a menudo pasamos por alto en estos debates es que cada lenguaje de programación y plataforma es criticado en función de ciertos problemas, que se crean por la forma en que usamos la plataforma. Independientemente de lo difícil que Node.js hace que escribir código seguro y lo fácil que hace escribir código altamente concurrente, la plataforma existe desde hace bastante tiempo y se ha utilizado para crear una gran cantidad de servicios web robustos y sofisticados. Estos servicios web escalan bien y han demostrado su estabilidad a través de su permanencia en Internet.

Sin embargo, como cualquier otra plataforma, Node.js es vulnerable a los problemas y problemas de los desarrolladores. Algunos de estos errores degradan el rendimiento, mientras que otros hacen que Node.js parezca inutilizable para lo que sea que esté tratando de lograr. En este artículo, veremos diez errores comunes que los desarrolladores nuevos en Node.js suelen cometer y cómo se pueden evitar para convertirse en un profesional de Node.js.

Errores del desarrollador de node.js

Error #1: Bloquear el bucle de eventos

JavaScript en Node.js (al igual que en el navegador) proporciona un entorno de un solo subproceso. Esto significa que no hay dos partes de su aplicación que se ejecuten en paralelo; en cambio, la simultaneidad se logra a través del manejo de operaciones enlazadas de E/S de forma asíncrona. Por ejemplo, una solicitud de Node.js al motor de la base de datos para obtener algún documento es lo que le permite a Node.js enfocarse en alguna otra parte de la aplicación:

 // Trying to fetch an user object from the database. Node.js is free to run other parts of the code from the moment this function is invoked.. db.User.get(userId, function(err, user) { // .. until the moment the user object has been retrieved here }) 

Entorno de subproceso único node.js

Sin embargo, una pieza de código vinculado a la CPU en una instancia de Node.js con miles de clientes conectados es todo lo que se necesita para bloquear el bucle de eventos, lo que hace que todos los clientes esperen. Los códigos vinculados a la CPU incluyen intentar clasificar una matriz grande, ejecutar un bucle extremadamente largo, etc. Por ejemplo:

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

Invocar esta función "sortUsersByAge" puede estar bien si se ejecuta en una matriz pequeña de "usuarios", pero con una matriz grande, tendrá un impacto horrible en el rendimiento general. Si esto es algo que absolutamente se debe hacer, y está seguro de que no habrá nada más esperando en el bucle de eventos (por ejemplo, si esto fuera parte de una herramienta de línea de comandos que está creando con Node.js, y no importaría si todo se ejecutara sincrónicamente), entonces esto puede no ser un problema. Sin embargo, en una instancia de servidor Node.js que intenta atender a miles de usuarios a la vez, este patrón puede resultar fatal.

Si esta matriz de usuarios se recuperara de la base de datos, la solución ideal sería recuperarla ya ordenada directamente de la base de datos. Si el bucle de eventos estaba siendo bloqueado por un bucle escrito para calcular la suma de un largo historial de datos de transacciones financieras, se podría diferir a alguna configuración de cola/trabajador externo para evitar acaparar el bucle de eventos.

Como puede ver, no existe una solución milagrosa para este tipo de problema de Node.js, sino que cada caso debe abordarse individualmente. La idea fundamental es no hacer un trabajo intensivo de la CPU en las instancias frontales de Node.js, a las que los clientes se conectan simultáneamente.

Error n.º 2: invocar una devolución de llamada más de una vez

JavaScript se ha basado en las devoluciones de llamada desde siempre. En los navegadores web, los eventos se manejan pasando referencias a funciones (a menudo anónimas) que actúan como devoluciones de llamada. En Node.js, las devoluciones de llamada solían ser la única forma en que los elementos asincrónicos de su código se comunicaban entre sí, hasta que se introdujeron las promesas. Las devoluciones de llamada todavía están en uso y los desarrolladores de paquetes aún diseñan sus API en torno a las devoluciones de llamada. Un problema común de Node.js relacionado con el uso de devoluciones de llamada es llamarlos más de una vez. Por lo general, una función proporcionada por un paquete para hacer algo de forma asíncrona está diseñada para esperar una función como su último argumento, que se llama cuando se completa la tarea asíncrona:

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

Observe cómo hay una declaración de devolución cada vez que se llama "hecho", hasta la última vez. Esto se debe a que llamar a la devolución de llamada no finaliza automáticamente la ejecución de la función actual. Si se comentó el primer "retorno", pasar una contraseña que no sea una cadena a esta función aún dará como resultado que se llame a "computeHash". Dependiendo de cómo "computeHash" se ocupe de tal escenario, "hecho" se puede llamar varias veces. Cualquiera que use esta función desde otro lugar puede quedar completamente desprevenido cuando la devolución de llamada que pasan se invoca varias veces.

Tener cuidado es todo lo que se necesita para evitar este error de Node.js. Algunos desarrolladores de Node.js adoptan el hábito de agregar una palabra clave de retorno antes de cada invocación de devolución de llamada:

 if(err) { return done(err) }

En muchas funciones asincrónicas, el valor devuelto casi no tiene importancia, por lo que este enfoque a menudo facilita evitar este problema.

Error n.º 3: devoluciones de llamada profundamente anidadas

Las devoluciones de llamada de anidamiento profundo, a menudo denominadas "infierno de devolución de llamada", no son un problema de Node.js en sí mismo. Sin embargo, esto puede causar problemas al hacer que el código se salga rápidamente de 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') }) }) }) } 

Infierno de devolución de llamada

Cuanto más compleja es la tarea, peor puede llegar a ser. Al anidar las devoluciones de llamada de tal manera, terminamos fácilmente con un código propenso a errores, difícil de leer y difícil de mantener. Una solución es declarar estas tareas como funciones pequeñas y luego vincularlas. Aunque, una de las (posiblemente) soluciones más limpias para esto es usar un paquete de utilidad Node.js que trata con patrones de JavaScript asincrónicos, como 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() { // ... }) }

Similar a "async.waterfall", hay una serie de otras funciones que proporciona Async.js para lidiar con diferentes patrones asincrónicos. Para abreviar, aquí usamos ejemplos más simples, pero la realidad suele ser peor.

Error n.º 4: esperar que las devoluciones de llamada se ejecuten sincrónicamente

La programación asincrónica con devoluciones de llamadas puede no ser algo exclusivo de JavaScript y Node.js, pero son responsables de su popularidad. Con otros lenguajes de programación, estamos acostumbrados al orden de ejecución predecible en el que dos declaraciones se ejecutarán una tras otra, a menos que haya una instrucción específica para saltar entre declaraciones. Incluso entonces, estos a menudo se limitan a declaraciones condicionales, declaraciones de bucle e invocaciones de funciones.

Sin embargo, en JavaScript, con las devoluciones de llamada, es posible que una función en particular no funcione bien hasta que finalice la tarea que está esperando. La ejecución de la función actual se ejecutará hasta el final sin detenerse:

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

Como notará, llamar a la función "testTimeout" primero imprimirá "Comenzar", luego imprimirá "Esperando ..." seguido del mensaje "¡Listo!" después de un segundo.

Cualquier cosa que deba suceder después de que se haya activado una devolución de llamada debe invocarse desde dentro.

Error #5: Asignar a "exportaciones", en lugar de "módulo.exportaciones"

Node.js trata cada archivo como un pequeño módulo aislado. Si su paquete tiene dos archivos, tal vez "a.js" y "b.js", entonces para que "b.js" acceda a la funcionalidad de "a.js", "a.js" debe exportarlo agregando propiedades a el objeto de exportación:

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

Cuando se hace esto, cualquier persona que requiera "a.js" recibirá un objeto con la función de propiedad "verifyPassword":

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

Sin embargo, ¿qué pasa si queremos exportar esta función directamente y no como propiedad de algún objeto? Podemos sobrescribir las exportaciones para hacer esto, pero no debemos tratarlo como una variable global entonces:

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

Observe cómo tratamos las "exportaciones" como una propiedad del objeto del módulo. La distinción aquí entre "module.exports" y "exportaciones" es muy importante y, a menudo, es motivo de frustración entre los nuevos desarrolladores de Node.js.

Error n.º 6: arrojar errores desde devoluciones de llamadas internas

JavaScript tiene la noción de excepciones. Imitando la sintaxis de casi todos los lenguajes tradicionales con soporte para el manejo de excepciones, como Java y C++, JavaScript puede "lanzar" y capturar excepciones en bloques de prueba y captura:

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

Sin embargo, try-catch no se comportará como cabría esperar en situaciones asincrónicas. Por ejemplo, si desea proteger una gran cantidad de código con mucha actividad asíncrona con un gran bloque de prueba y captura, no necesariamente funcionaría:

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

Si la devolución de llamada a "db.User.get" se activa de forma asíncrona, el alcance que contiene el bloque try-catch se habría salido de contexto hace mucho tiempo para que aún pudiera detectar esos errores generados desde dentro de la devolución de llamada.

Así es como los errores se manejan de una manera diferente en Node.js, y eso hace que sea esencial seguir el patrón (err, …) en todos los argumentos de la función de devolución de llamada: se espera que el primer argumento de todas las devoluciones de llamada sea un error si ocurre uno .

Error #7: Asumir que el número es un tipo de datos entero

Los números en JavaScript son puntos flotantes: no hay un tipo de datos entero. No esperaría que esto sea un problema, ya que los números lo suficientemente grandes como para enfatizar los límites de flotación no se encuentran a menudo. Ahí es exactamente cuando ocurren los errores relacionados con esto. Dado que los números de coma flotante solo pueden contener representaciones de enteros hasta un cierto valor, exceder ese valor en cualquier cálculo inmediatamente comenzará a estropearlo. Por extraño que parezca, lo siguiente se evalúa como verdadero en Node.js:

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

Desafortunadamente, las peculiaridades de los números en JavaScript no terminan aquí. Aunque los números son puntos flotantes, los operadores que funcionan con tipos de datos enteros también funcionan aquí:

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

Sin embargo, a diferencia de los operadores aritméticos, los operadores bit a bit y los operadores de cambio funcionan solo en los 32 bits finales de números "enteros" tan grandes. Por ejemplo, tratar de desplazar "Math.pow(2, 53)" en 1 siempre se evaluará como 0. Intentar hacer un bit a bit o de 1 con ese mismo número grande se evaluará como 1.

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

Es posible que rara vez necesite manejar números grandes, pero si lo hace, hay muchas bibliotecas de enteros grandes que implementan las operaciones matemáticas importantes en números de gran precisión, como node-bigint.

Error #8: Ignorar las ventajas de las API de transmisión

Digamos que queremos construir un pequeño servidor web similar a un proxy que responda a las solicitudes al obtener el contenido de otro servidor web. Como ejemplo, construiremos un pequeño servidor web que sirva imágenes de 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)

En este ejemplo particular de un problema de Node.js, estamos obteniendo la imagen de Gravatar, leyéndola en un búfer y luego respondiendo a la solicitud. Esto no es algo tan malo, dado que las imágenes de Gravatar no son demasiado grandes. Sin embargo, imagine si el tamaño de los contenidos que estamos transmitiendo fuera de miles de megabytes. Un enfoque mucho mejor hubiera sido este:

 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)

Aquí, buscamos la imagen y simplemente canalizamos la respuesta al cliente. En ningún momento necesitamos leer todo el contenido en un búfer antes de servirlo.

Error n.º 9: utilizar Console.log con fines de depuración

En Node.js, "console.log" le permite imprimir casi cualquier cosa en la consola. Pase un objeto y lo imprimirá como un objeto literal de JavaScript. Acepta cualquier número arbitrario de argumentos y los imprime todos claramente separados por espacios. Hay varias razones por las que un desarrollador puede sentirse tentado a usar esto para depurar su código; sin embargo, se recomienda encarecidamente que evite "console.log" en código real. Debe evitar escribir "console.log" en todo el código para depurarlo y luego comentarlos cuando ya no sean necesarios. En su lugar, utilice una de las increíbles bibliotecas creadas solo para esto, como debug.

Paquetes como estos brindan formas convenientes de habilitar y deshabilitar ciertas líneas de depuración cuando inicia la aplicación. Por ejemplo, con debug es posible evitar que se impriman líneas de depuración en el terminal al no establecer la variable de entorno DEBUG. Usarlo es simple:

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

Para habilitar las líneas de depuración, simplemente ejecute este código con la variable de entorno DEBUG establecida en "aplicación" o "*":

 DEBUG=app node app.js

Error #10: No Usar Programas de Supervisor

Independientemente de si su código Node.js se ejecuta en producción o en su entorno de desarrollo local, es extremadamente útil tener un monitor de programa supervisor que pueda orquestar su programa. Una práctica recomendada a menudo por los desarrolladores que diseñan e implementan aplicaciones modernas recomienda que su código falle rápidamente. Si ocurre un error inesperado, no intente manejarlo, sino que deje que su programa se bloquee y haga que un supervisor lo reinicie en unos segundos. Los beneficios de los programas de supervisor no se limitan solo a reiniciar programas bloqueados. Estas herramientas le permiten reiniciar el programa cuando falla, así como reiniciarlo cuando cambian algunos archivos. Esto hace que desarrollar programas Node.js sea una experiencia mucho más placentera.

Hay una gran cantidad de programas de supervisión disponibles para Node.js. Por ejemplo:

  • pm2

  • Siempre

  • nodemonio

  • supervisor

Todas estas herramientas vienen con sus pros y sus contras. Algunos de ellos son buenos para manejar múltiples aplicaciones en la misma máquina, mientras que otros son mejores en la gestión de registros. Sin embargo, si desea comenzar con un programa de este tipo, todas estas son opciones justas.

Conclusión

Como puede ver, algunos de estos problemas de Node.js pueden tener efectos devastadores en su programa. Algunos pueden ser la causa de la frustración mientras intenta implementar las cosas más simples en Node.js. Aunque Node.js ha hecho que sea extremadamente fácil para los recién llegados comenzar, todavía tiene áreas en las que es igual de fácil cometer errores. Los desarrolladores de otros lenguajes de programación pueden relacionarse con algunos de estos problemas, pero estos errores son bastante comunes entre los nuevos desarrolladores de Node.js. Afortunadamente, son fáciles de evitar. Espero que esta breve guía ayude a los principiantes a escribir mejor código en Node.js y a desarrollar software estable y eficiente para todos nosotros.