Uma máxima no desenvolvimento de software é que as coisas darão errado. Nada é mais certo do que isso, principalmente quando começamos a deixar os Hello Worlds e CRUDs básicos. Saber como analisar e tratar os erros do seu software é algo fundamental se você quiser progredir em seus projetos e carreira e apesar deste tópico ser agnóstico de tecnologia, eu quero propor hoje uma abordagem de estudo focado em desenvolvedores que trabalham com a stack JavaScript, que tem sido o meu foco desde 2016.
Assim, neste artigo sobre JavaScript quero tratar os principais fundamentos da análise e tratamento de erros em JavaScript (também chamado de Error Handling), principalmente falando de stacktrace mas incluindo também as estruturas de tratamento (try/catch/finally), cheio de exemplos reais para analisarmos juntos.
Vamos lá!
JavaScript – Alocação de Memória
A primeira coisa que você precisa entender, antes mesmo de estudar os erros, é sobre os conceitos básicos de alocação de memória.
Geralmente nossos algoritmos vão ter inputs, um processamento deles e outputs, correto? Esses inputs ou dados de entrada precisam ser armazenados em memória antes do processamento ocorrer, momento onde serão acessados e, mais tarde, após o processamento, serão liberados. Para que todos esses inputs não se percam na memória do computador, quando um programa é executado, o runtime aloca duas áreas de memória chamadas de heap e de stack, responsável por manter tudo que o programa precisa e enquanto ele precisar.
A Heap é uma área de memória dinâmica, utilizada para alocação de dados de tamanho variável, por exemplo o conteúdo de arrays, de funções e o conteúdo de objetos. Funções e objetos podem ter tamanhos extremamente variáveis .
Stack e JavaScript
Já a Stack é uma área de memória estática, utilizada para alocação de dados de tamanho fixo como as primitivas do JavaScript (number, string, etc) e referências para funções e objetos. Repare que em ambas eu cito objetos e funções, mas enquanto na primeira eu falo de conteúdo, na segunda eu falo de referência. Uma referência de memória (ou ponteiro/pointer) é apenas o registro na stack de um local na heap, como mostra a imagem abaixo, onde a variável de referência “names” na stack aponta para um array de strings na heap.
Dessa forma, você pode entender a stack como um índice da heap, mas tem mais algumas coisas sobre ela que você precisa saber, sendo a principal, como que sua alocação se dá, sendo que ela não possui o nome de “stack” à toa.
Stack significa “pilha” e esse nome vem em alusão às pilhas do mundo real, onde colocamos um objeto em cima do outro como em uma pilha de pratos ou de livros. Então conforme você vai declarando suas variáveis, seus valores (no caso de primitivas) ou suas referências (no caso de objetos e funções), vão sendo empilhados na memória stack, um em cima do outro.
Mais tarde, conforme as funções terminam e as variáveis não são mais necessárias, inicia-se o processo de “desempilhar”, ou seja, vamos removendo da stack as referências e variáveis, uma a uma, seguindo a ordem LIFO ou Last In, First Out (o último a entrar, é o primeiro a sair). Quando o último elemento é removido e a stack fica vazia, o programa é encerrado.
Mas e se algo dá errado durante o processamento?
JavaScript – Stack Trace
Quando um comportamento inesperado acontece, é o que chamamos de exceção. As exceções são objetos de erro enviados pelo runtime e que interrompem o fluxo natural do processamento caso não sejam tratados. Junto à mensagem da exceção é incluído sempre o rastro da pilha ou stack trace. E é na análise desses dois elementos que você vai se basear para entender o que houve e poder tratar ou solucionar a exceção mais tarde.
O stack trace nada mais é do que um resumo do estado da stack no momento da exceção, ou mais simplesmente: as últimas funções chamadas antes do erro acontecer, na ordem inversa em que foram executadas (é uma pilha, lembra?). A parte boa é que além de te dar o nome da função, o rastro da pilha te fornece o caminho completo até o arquivo, linha e coluna onde a mesma foi disparada, veja o exemplo abaixo, um caso real que aconteceu com um aluno.
O tipo da exceção foi TypeError e a mensagem dela é “Cannot read properties of null (reading ‘isSystemMon’)”. Apenas com essa mensagem, se traduzida para o português, já podemos deduzir o que aconteceu: seu programa tentou acessar uma propriedade isSystemMon de um objeto null, causando o erro. No entanto ficam algumas dúvidas: qual instrução tentou ler? De qual arquivo? Em qual função?
Aí que entra o rastro da pilha. Repare que temos dois elementos na stack, nas linhas iniciadas por “at”, sendo que o mais ao topo é sempre o último que foi chamado antes do erro acontecer, neste caso, a chamada de função deleteMonitor. Ao lado do nome da função, você encontra o caminho completo até o arquivo que disparou a exceção (monitorsController.js) e ao lado dele a linha e coluna onde a propriedade tentou ser lida, separados por dois pontos (linha 76 e coluna 24, neste exemplo).
LEIA TAMBÉM: Entendendo os laços “for” do JavaScript para interação com arrays
Apenas com essa primeira linha do rastro, neste caso, já é possível investigar o problema que pode ser tratado, por exemplo, com um simples if ou mesmo revisando a origem do objeto para entender porque ele está null. Ainda assim, repare que o stack trace tem ainda mais uma linha, mas que ela é do processo central do runtime (process) e portanto foge da nossa alçada, não temos controle algum sobre ela. Mesmo que você vá nos fontes dela (task_queues) não poderá “resolver” nada e em 90% dos casos (salvo bugs no runtime ou em libs) não é onde está o problema.
Sendo assim, a maior dica aqui é: procure sempre investigar o(s) elemento(s) mais ao topo do stack trace, ignorando quaisquer funções que não estejam em arquivos que você programou. No máximo, analise arquivos de libs que você usa, mas já sabendo que se encontrar problemas aqui vai ter abrir issue no GitHub da lib ou submeter pull request se você mesmo quiser solucionar.
Aqui mais um, para você exercitar. Tente achar na imagem a mensagem de erro e o arquivo, linha e coluna onde ele foi disparado.
Repare que este é exatamente o mesmo erro que o anterior, mas em uma chamada de função split e em outro arquivo, o app-em.js, linha 179. Neste caso o Timeout não é uma função do usuário, mas sim um timer que ele criou e que quando foi disparar, deu o erro em questão.
Mais um, pra ficar fera, tente analisar antes de ler a resposta a seguir:
Repare que aqui temos um erro de que um objeto broadcastLabel não está definido (um ReferenceError) e que o processamento começou através de um timer no app-em.js linha 145 que por sua vez chamou uma função startUserDataMonitor no mesmo arquivo, mas na linha 102. Ou seja, você entende a execução de baixo para cima, sendo a função mais no topo a que disparou o erro.
JavaScript – Outros Tipos de Erros
Mas Luiz, e se o erro é no frontend, em uma página React, por exemplo?
Vamos pegar um caso prático, pois a ideia é a mesma.
Repare que aqui o erro é bem parecido, um “Cannot read properties of undefined (reading ‘map’)”, ou seja, tentou fazer um map em cima de um objeto undefined. Na renderização do componente Monitors (ali no topo), é onde aconteceu o problema, então você deve procurar no Monitors.js, função Monitors na linha 85 pelo problema de renderização que causou esse erro.
Mas e se o erro não tem nenhum arquivo que seja meu para investigar?
Vamos pegar um caso prático, para ficar bem didático.
Neste caso aqui repare a mensagem de erro: uma junção de palavras tudo em maiúsculas iniciada com E, de ERROR. Mais especificamente, neste caso tivemos erro de Socket Timed Out, ou expiração de tempo de socket, mas não vem ao caso os detalhes desse erro em si, caso você não entenda de sockets. O que importa aqui é olhar o stack trace e ver que não tem absolutamente nenhum arquivo ou função que seja do programador que criou o sistema, mas sim apenas chamadas de runtime (baixo nível). Mensagens com títulos em maiúscula são erros de baixo nível, geralmente causados por problemas na sua infraestrutura (Internet, rede, permissões, etc) ou na configuração da sua aplicação (URL, Portas, etc), então eu recomendo nestes casos que olhe para arquivos .env, revise sua máquina e só depois olhe para seu código para ver se ele está errado.
LEIA TAMBÉM: Descomplicando call, apply e bind em JavaScript
Mas Luiz, sempre tem um stack trace?
No caso de exceções não tratadas, sim. Mas se o programador tratou a exceção, capturando ela e optando por mandar uma mensagem “amigável” para o usuário, você provavelmente não terá o stack trace. Nestes casos, terá de entender o contexto do comando que executou para entender melhor como resolver o erro. Isso é muito comum em CLIs por exemplo (as ferramentas de linha de comando), como no exemplo abaixo do Sequelize CLI.
Repare o erro ao final: “Unknown column indexes in field list” ou seja, uma coluna indexes não foi reconhecida na lista de campos. Para entender isso você deve olhar o contexto do erro, já que um pouco antes tem um código SQL enorme e lá no topo dá pra ver que o programador chamou o comando “npx sequelize-cli db:seed:all” ou seja, ele tentou executar uma seeder no banco de dados, gerando o erro. Logo, provavelmente é um erro de programação na seeder dele, que está tentando chamar um campo que não existe na tabela em questão ou um erro na tabela em si, que foi criada com campo incorreto.
Vamos a mais um, nessa mesma linha de erro em CLI, novamente com Sequelize CLI:
Aqui seguimos a mesma lógica: primeiro a mensagem, depois o contexto. A mensagem nos diz que o acesso foi negado para o usuário root na máquina localhost, mesmo tendo usado a senha (password: YES). Olhando o contexto, é um comando “npx sequelize-cli db:create”, ou seja, uma tentativa de criação do banco de dados que teve acesso negado, mesmo usando senha. Neste caso, ou a senha usada está errada ou o usuário não tem permissão, mas se tratando que é usuário root, provavelmente é senha errada mesmo.
Então sim, mesmo que você não tenha stacktrace, é possível entender o que causou o erro analisando a sua mensagem e o contexto em que ele foi disparado.
LEIA TAMBÉM: Criando um CryptoBubbles-clone com JavaScript
Try/Catch/Finally
Uma vez que você tenha entendido a mensagem de erro o ideal é você corrigir a causa do mesmo. Eu sei que parece óbvio isso, mas falo porque vão ter ocasiões em que não será possível corrigir, quer porque o disparo é proposital (algo até comum), quer porque o problema é em um código que não é seu (lib de terceiros com bug, por exemplo). Como exemplo posso citar a lib Axios, que dispara propositalmente exceções sempre que um retorno à uma chamada HTTP não for um sucesso (código 2xx). Nessas horas, o melhor caminho é capturar e tratar o erro.
Para fazer a captura de uma exceção você deve usar um bloco try/catch, como abaixo.
1
2
3
4
5
6
|
try {
//código que dispara exception
}
catch(e){
//tratamento da exception
}
|
A seção try deve envolver o trecho de código que dispara a exception. Qualquer exception disparada dentro de um bloco try será capturada e irá redirecionar o fluxo de execução para o bloco catch, mais ao final. Dentro do bloco catch você pode adicionar o seu tratamento ao problema, como o ajuste de alguma variável, uma nova chamada ou ainda o registro do erro para investigação posterior (com Winston localmente ou AWS CloudWatch na nuvem).
Caso necessite, o objeto ‘e’ do catch é do tipo Error e possui um ‘message’ com a mensagem de erro e um ‘stacktrace’ com o rastro da pilha, assim como você veria no terminal caso deixasse o erro estourar.
Após o tratamento no bloco catch, o programa seguirá o fluxo logo após o mesmo, ignorando qualquer outro comando que estava definido para ser executado dentro do bloco try. Caso você queira que algo seja executado mesmo em caso de erro, é o caso de adicionar mais um bloco finally, após o catch, como abaixo.
1
2
3
4
5
6
7
8
9
|
try {
//código que dispara exception
}
catch(e){
//tratamento da exception
}
finally {
//será executado não importa a exception
}
|
O finally é um comando tão prioritário que mesmo que você tenha uma exception acontecendo dentro do try ou mesmo uma cláusula return, o conteúdo do finally será executado.
Dependendo das tecnologias que você estiver usando em seu projeto pode ser que você tenha outras opções de captura de erros, como no caso do Error Middleware do Express, que falo nesse outro tutorial.
Um abraço e até a próxima!