Os 10 erros mais comuns que os desenvolvedores Node.js cometem
Publicados: 2022-03-11Desde o momento em que o Node.js foi revelado ao mundo, ele recebeu muitos elogios e críticas. O debate ainda continua e pode não terminar tão cedo. O que muitas vezes ignoramos nesses debates é que toda linguagem de programação e plataforma é criticada com base em certas questões, que são criadas pela forma como usamos a plataforma. Independentemente de quão difícil o Node.js torna escrever código seguro, e quão fácil ele torna escrever código altamente simultâneo, a plataforma existe há bastante tempo e tem sido usada para construir um grande número de serviços web robustos e sofisticados. Esses serviços da Web são bem dimensionados e provaram sua estabilidade por meio de sua resistência ao tempo na Internet.
No entanto, como qualquer outra plataforma, o Node.js é vulnerável a problemas e problemas do desenvolvedor. Alguns desses erros prejudicam o desempenho, enquanto outros fazem o Node.js parecer inutilizável para o que você está tentando alcançar. Neste artigo, veremos dez erros comuns que os desenvolvedores novos no Node.js costumam cometer e como eles podem ser evitados para se tornar um profissional do Node.js.
Erro nº 1: Bloqueando o loop de eventos
JavaScript no Node.js (assim como no navegador) fornece um ambiente de encadeamento único. Isso significa que duas partes do seu aplicativo não são executadas em paralelo; em vez disso, a simultaneidade é alcançada por meio do tratamento de operações vinculadas de E/S de forma assíncrona. Por exemplo, uma solicitação do Node.js ao mecanismo de banco de dados para buscar algum documento é o que permite que o Node.js se concentre em alguma outra parte do aplicativo:
// 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 })
No entanto, basta um pedaço de código vinculado à CPU em uma instância do Node.js com milhares de clientes conectados para bloquear o loop de eventos, fazendo com que todos os clientes esperem. Os códigos vinculados à CPU incluem tentar classificar uma matriz grande, executar um loop extremamente longo e assim por diante. Por exemplo:
function sortUsersByAge(users) { users.sort(function(a, b) { return a.age < b.age ? -1 : 1 }) }
Invocar esta função “sortUsersByAge” pode ser bom se executado em uma pequena matriz de “usuários”, mas com uma matriz grande, terá um impacto terrível no desempenho geral. Se isso é algo que absolutamente deve ser feito e você tem certeza de que não haverá mais nada esperando no loop de eventos (por exemplo, se isso for parte de uma ferramenta de linha de comando que você está construindo com Node.js, e não importaria se a coisa toda fosse executada de forma síncrona), isso pode não ser um problema. No entanto, em uma instância de servidor Node.js tentando atender milhares de usuários ao mesmo tempo, esse padrão pode ser fatal.
Se esse array de usuários estivesse sendo recuperado do banco de dados, a solução ideal seria buscá-lo já ordenado diretamente do banco de dados. Se o loop de eventos estivesse sendo bloqueado por um loop escrito para calcular a soma de um longo histórico de dados de transações financeiras, ele poderia ser adiado para alguma configuração de fila/trabalhador externo para evitar sobrecarregar o loop de eventos.
Como você pode ver, não existe uma solução mágica para esse tipo de problema do Node.js, em vez disso, cada caso precisa ser tratado individualmente. A ideia fundamental é não fazer trabalho intensivo de CPU nas instâncias do Node.js voltadas para a frente - aquelas às quais os clientes se conectam simultaneamente.
Erro nº 2: invocar um retorno de chamada mais de uma vez
JavaScript depende de retornos de chamada desde sempre. Nos navegadores da Web, os eventos são tratados passando referências a funções (geralmente anônimas) que agem como retornos de chamada. No Node.js, os retornos de chamada costumavam ser a única maneira pela qual os elementos assíncronos do seu código se comunicavam entre si - até que as promessas fossem introduzidas. Os retornos de chamada ainda estão em uso e os desenvolvedores de pacotes ainda projetam suas APIs em torno de retornos de chamada. Um problema comum do Node.js relacionado ao uso de retornos de chamada é chamá-los mais de uma vez. Normalmente, uma função fornecida por um pacote para fazer algo de forma assíncrona é projetada para esperar uma função como seu último argumento, que é chamado quando a tarefa assíncrona é concluída:
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 como há uma instrução de retorno toda vez que “done” é chamado, até a última vez. Isso ocorre porque chamar o retorno de chamada não encerra automaticamente a execução da função atual. Se o primeiro “retorno” foi comentado, passar uma senha que não seja string para esta função ainda resultará na chamada de “computeHash”. Dependendo de como “computeHash” lida com esse cenário, “done” pode ser chamado várias vezes. Qualquer pessoa usando esta função de outro lugar pode ser pega completamente desprevenida quando o retorno de chamada que eles passam é invocado várias vezes.
Ser cuidadoso é o suficiente para evitar esse erro do Node.js. Alguns desenvolvedores do Node.js adotam o hábito de adicionar uma palavra-chave return antes de cada chamada de retorno:
if(err) { return done(err) }
Em muitas funções assíncronas, o valor de retorno quase não tem significado, portanto, essa abordagem geralmente facilita evitar esse problema.
Erro nº 3: retornos de chamada profundamente aninhados
Os retornos de chamada de aninhamento profundo, geralmente chamados de "inferno de retorno de chamada", não são um problema do Node.js em si. No entanto, isso pode causar problemas, tornando o código rapidamente fora de controle:
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') }) }) }) }
Quanto mais complexa a tarefa, pior isso pode ficar. Ao aninhar retornos de chamada dessa maneira, acabamos facilmente com código propenso a erros, difícil de ler e difícil de manter. Uma solução é declarar essas tarefas como pequenas funções e vinculá-las. Embora, uma das soluções (sem dúvida) mais limpas para isso seja usar um pacote utilitário Node.js que lida com padrões JavaScript assíncronos, 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() { // ... }) }
Semelhante ao “async.waterfall”, há várias outras funções que o Async.js fornece para lidar com diferentes padrões assíncronos. Para resumir, usamos exemplos mais simples aqui, mas a realidade geralmente é pior.
Erro nº 4: Esperando que os retornos de chamada sejam executados de forma síncrona
A programação assíncrona com callbacks pode não ser algo exclusivo do JavaScript e do Node.js, mas são responsáveis por sua popularidade. Com outras linguagens de programação, estamos acostumados à ordem previsível de execução em que duas instruções serão executadas uma após a outra, a menos que haja uma instrução específica para pular entre as instruções. Mesmo assim, eles geralmente são limitados a instruções condicionais, instruções de loop e invocações de função.
No entanto, em JavaScript, com retornos de chamada, uma função específica pode não funcionar bem até que a tarefa que ela está aguardando seja concluída. A execução da função atual será executada até o final sem nenhuma parada:
function testTimeout() { console.log(“Begin”) setTimeout(function() { console.log(“Done!”) }, duration * 1000) console.log(“Waiting..”) }
Como você notará, chamar a função “testTimeout” primeiro imprimirá “Begin”, depois imprimirá “Waiting..” seguido pela mensagem “Done!” após cerca de um segundo.
Qualquer coisa que precise acontecer depois que um retorno de chamada for disparado precisa ser invocado de dentro dele.
Erro nº 5: Atribuir a “exports”, em vez de “module.exports”
O Node.js trata cada arquivo como um pequeno módulo isolado. Se o seu pacote tiver dois arquivos, talvez “a.js” e “b.js”, então para que “b.js” acesse a funcionalidade de “a.js”, “a.js” deve exportá-lo adicionando propriedades a o objeto de exportação:

// a.js exports.verifyPassword = function(user, password, done) { ... }
Quando isso for feito, qualquer pessoa que precisar de “a.js” receberá um objeto com a função de propriedade “verifyPassword”:
// b.js require('a.js') // { verifyPassword: function(user, password, done) { ... } }
No entanto, e se quisermos exportar essa função diretamente, e não como propriedade de algum objeto? Podemos sobrescrever as exportações para fazer isso, mas não devemos tratá-la como uma variável global:
// a.js module.exports = function(user, password, done) { ... }
Observe como estamos tratando “exports” como uma propriedade do objeto módulo. A distinção aqui entre “module.exports” e “exports” é muito importante e muitas vezes causa frustração entre os novos desenvolvedores Node.js.
Erro nº 6: Lançando erros de retornos de chamada internos
JavaScript tem a noção de exceções. Imitando a sintaxe de quase todas as linguagens tradicionais com suporte a manipulação de exceções, como Java e C++, JavaScript pode “lançar” e capturar exceções em blocos try-catch:
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!') }
No entanto, try-catch não se comportará como você poderia esperar em situações assíncronas. Por exemplo, se você quisesse proteger um grande pedaço de código com muita atividade assíncrona com um grande bloco try-catch, isso não funcionaria necessariamente:
try { db.User.get(userId, function(err, user) { if(err) { throw err } // ... usernameSlug = slugifyUsername(user.username) // ... }) } catch(e) { console.log('Oh no!') }
Se o retorno de chamada para “db.User.get” fosse disparado de forma assíncrona, o escopo contendo o bloco try-catch teria saído do contexto há muito tempo para que ele ainda pudesse capturar esses erros lançados de dentro do retorno de chamada.
É assim que os erros são tratados de maneira diferente no Node.js, e isso torna essencial seguir o padrão (err, …) em todos os argumentos da função de retorno de chamada - espera-se que o primeiro argumento de todos os retornos de chamada seja um erro se ocorrer .
Erro nº 7: Supondo que o número seja um tipo de dados inteiro
Números em JavaScript são pontos flutuantes - não há tipo de dados inteiro. Você não esperaria que isso fosse um problema, pois números grandes o suficiente para enfatizar os limites de float não são encontrados com frequência. É exatamente quando os erros relacionados a isso acontecem. Como os números de ponto flutuante só podem conter representações inteiras até um determinado valor, exceder esse valor em qualquer cálculo começará imediatamente a atrapalhar. Por mais estranho que possa parecer, o seguinte é avaliado como verdadeiro no Node.js:
Math.pow(2, 53)+1 === Math.pow(2, 53)
Infelizmente, as peculiaridades com números em JavaScript não terminam aqui. Embora os números sejam pontos flutuantes, os operadores que funcionam em tipos de dados inteiros também funcionam aqui:
5 % 2 === 1 // true 5 >> 1 === 2 // true
No entanto, ao contrário dos operadores aritméticos, os operadores bit a bit e os operadores de deslocamento funcionam apenas nos 32 bits finais desses números "inteiros" grandes. Por exemplo, tentar mudar “Math.pow(2, 53)” por 1 sempre resultará em 0. Tentar fazer um bit a bit-ou de 1 com esse mesmo número grande resultará em 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
Você raramente precisa lidar com números grandes, mas se o fizer, há muitas bibliotecas de números inteiros grandes que implementam as operações matemáticas importantes em números de grande precisão, como node-bigint.
Erro nº 8: ignorar as vantagens das APIs de streaming
Digamos que queremos construir um pequeno servidor web semelhante a um proxy que atenda a requisições buscando o conteúdo de outro servidor web. Como exemplo, vamos construir um pequeno servidor web que serve imagens do 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)
Neste exemplo específico de um problema Node.js, estamos buscando a imagem do Gravatar, lendo-a em um Buffer e respondendo à solicitação. Isso não é uma coisa tão ruim de se fazer, já que as imagens do Gravatar não são muito grandes. No entanto, imagine se o tamanho do conteúdo que estamos fazendo proxy fosse de milhares de megabytes. Uma abordagem muito melhor teria sido esta:
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)
Aqui, buscamos a imagem e simplesmente canalizamos a resposta para o cliente. Em nenhum momento precisamos ler todo o conteúdo em um buffer antes de servi-lo.
Erro nº 9: usando o Console.log para fins de depuração
No Node.js, “console.log” permite imprimir quase tudo no console. Passe um objeto para ele e ele o imprimirá como um literal de objeto JavaScript. Ele aceita qualquer número arbitrário de argumentos e os imprime perfeitamente separados por espaços. Existem várias razões pelas quais um desenvolvedor pode se sentir tentado a usar isso para depurar seu código; no entanto, é altamente recomendável que você evite “console.log” em código real. Você deve evitar escrever “console.log” em todo o código para depurá-lo e depois comentá-los quando não forem mais necessários. Em vez disso, use uma das bibliotecas incríveis criadas apenas para isso, como debug.
Pacotes como esses fornecem maneiras convenientes de habilitar e desabilitar certas linhas de depuração quando você inicia o aplicativo. Por exemplo, com debug, é possível evitar que qualquer linha de depuração seja impressa no terminal, não configurando a variável de ambiente DEBUG. Usá-lo é simples:
// app.js var debug = require('debug')('app') debug('Hello, %s!', 'world')
Para habilitar as linhas de depuração, basta executar este código com a variável de ambiente DEBUG definida como “app” ou “*”:
DEBUG=app node app.js
Erro nº 10: não usar programas de supervisor
Independentemente de seu código Node.js estar sendo executado em produção ou em seu ambiente de desenvolvimento local, é extremamente útil ter um monitor de programa supervisor que possa orquestrar seu programa. Uma prática frequentemente recomendada por desenvolvedores que projetam e implementam aplicativos modernos recomenda que seu código falhe rapidamente. Se ocorrer um erro inesperado, não tente manipulá-lo, em vez disso, deixe seu programa travar e peça para um supervisor reiniciá-lo em alguns segundos. Os benefícios dos programas supervisores não se limitam apenas à reinicialização de programas travados. Essas ferramentas permitem que você reinicie o programa em caso de falha, bem como reinicie-os quando alguns arquivos forem alterados. Isso torna o desenvolvimento de programas Node.js uma experiência muito mais agradável.
Há uma infinidade de programas de supervisor disponíveis para Node.js. Por exemplo:
pm2
para todo sempre
nómon
Supervisor
Todas essas ferramentas vêm com seus prós e contras. Alguns deles são bons para lidar com vários aplicativos na mesma máquina, enquanto outros são melhores no gerenciamento de logs. No entanto, se você quiser começar com esse programa, todas essas são escolhas justas.
Conclusão
Como você pode ver, alguns desses problemas do Node.js podem ter efeitos devastadores em seu programa. Alguns podem ser a causa da frustração enquanto você tenta implementar as coisas mais simples no Node.js. Embora o Node.js tenha tornado extremamente fácil para os recém-chegados começarem, ele ainda tem áreas em que é igualmente fácil errar. Desenvolvedores de outras linguagens de programação podem se relacionar com alguns desses problemas, mas esses erros são bastante comuns entre os novos desenvolvedores Node.js. Felizmente, eles são fáceis de evitar. Espero que este pequeno guia ajude os iniciantes a escrever um código melhor em Node.js e a desenvolver um software estável e eficiente para todos nós.