Error handler no express

Matheus Vellone
stonetech
5 min readMar 27, 2020

--

Algo que tenho visto em alguns projetos é o uso do res.send para responder requests de erro, algo nesse formato.

O código em si, não apresenta nenhum problema, mas existe o risco de que aquele return dentro do if da linha 5 não existir, por descuido do desenvolvedor e/ou do processo de review. A falta desse return faria com que

Para minimizar esse risco, podemos substituir essa linha por um throw de um erro customizado.

Mas esse erro, sozinho, não é capaz de responder a request com o statusCode correto, nem de montar o JSON de resposta que queremos. Se somente fizermos isso, a request será respondida com erro 500.

Este artigo vai te ensinar, na prática, como montar um error handler capaz de capturar estes erros e responder de acordo com o erro lançado.

Hierarquia de erros customizados

Primeiramente, vamos precisar de uma hierarquia de erros que tenha informações suficientes para responder uma request e que pertença a um erro base, você vai entender o porquê em breve.

Vamos então ao erro base. Ele precisa ser capaz de salvar informações gerais de um erro, e que sejam suficientes para responder uma request, portanto vamos considerar o statusCode, um código de erro e uma mensagem.

Usando um pouco de ramda, estamos buscando os códigos de erro a partir da variável errorCodes, que é um JSON com o seguinte formato.

Dessa forma, sempre que chamarmos o BaseError, passamos uma string no construtor que servirá para recuperar o código do erro. O intuito desse código de erro é fornecer mais informações para o usuário sobre o erro que aconteceu, pois não conseguimos usar o código HTTP 404 para diferenciar o erro de “usuário não encontrado” do erro “produto não encontrado”.

Agora, com o erro base criado, todos os erros que quisermos criar precisam herdar da classe BaseError.

É importante manter os erros (essas classes que herdam o BaseError) genéricos o suficiente para que seu projeto não tenha 172 erros diferentes que são utilizados apenas uma vez no código. Recomendo criar um erro para cada código HTTP conforme você for precisando deles e reutilizar o erro sempre que necessário.

E com o erro criado, estamos prontos para dar throw new NotFoundError('user'), mas ainda precisamos criar o error handler para capturar este erro e responder a request de acordo com o erro lançado.

Error Handler

Este será o nosso error handler

A função do error hander recebe 4 parâmetros, com o primeiro sendo o próprio erro e os outros parâmetros, req, res e next, representam respectivamente o objeto da request, o objeto utilizado para lidar com a resposta e uma função para finalizar a execução e continua com o fluxo da request na aplicação.

Como o erro pode vir de qualquer lugar e pode ter qualquer valor, precisamos "normalizar" o erro, que é a responsabilidade da função normalizeError: com o erro original (recebido pelo error handler), transformar o erro em um erro com informações suficientes para responder uma request . Em outras palavras, essa função transforma o erro original em um erro que faça parte da hierarquia de erros que criamos anteriormente (BaseError), assim, poderemos usar o retorno dessa função para responder a request, que é o que o error handler termina de fazer.

A função normalizeError também trata erros inesperados. Quando um erro que não seja instanceof BaseError é capturado, a função cria um erro interno, idealmente com código HTTP 500 e que também extends BaseError, e o retorna. Assim temos a garantia de que qualquer erro inesperado será respondido sempre da mesma forma.

Recomendação de como implementar o InternalServerError

Dica: o construtor do InternalServerError pode receber o erro original e logá-lo, assim, além da garantia da interface de resposta, temos também a padronização de que estes erros serão logados.

Até agora só criamos o error handler, para adicioná-lo basta adicionar o seguinte código.

Importante: é necessário que o error handler seja aplicado ao app após todas as declarações de rotas e middleware, pois o error handler só é aplicado a rotas que foram criadas antes dele.

Agora que temos os erros e o error handler, só nos resta conectar o nosso controller, que é quem vai manipular e dar throw dos nossos erros com o error handler em si.

Para os erros serem direcionados para o nosso error handler, precisamos chamar a função do terceiro parâmetro (next) do controller sempre que um erro acontecer. Para isso, vamos precisar que os controllers tenham uma estrutura dessa forma.

E sempre que um erro for lançado e capturado pelo nosso try/catch, estaremos redirecionando ele para o error handler, que vai tratar o erro e responder a request. Assim, podemos dar um throw de qualquer lugar do nosso código, que ele seguirá caminho até o error handler sem precisar nos preocuparmos.

Sugestões para manter o código mais organizado

Conforme a API vai crescendo e mais rotas vão sendo criadas, teremos cada vez mais a repetição desse try/catch. E repetição de código geralmente indica uma oportunidade de melhoria do código, quando não uma má arquitetura. Nesse caso, podemos criar uma High Order Function para evitar repetição de código.

Nossa HOF será assim:

E sempre que formos declarar uma rota, aplicar essa HOF na função do controller. Dessa forma, evitamos a repetição do block try/catch.

Uma coisa que já vi acontecer em projetos grandes, é passar o objeto res para dentro da arquitetura (services, repositories e por aí vai) e responder a request de um arquivo que não é o controller, o que é bem estranho, pois é responsabilidade do controller responder a requisição, idealmente, é ele quem faz essa conexão entre o endpoint REST e a nossa regra de negócio.

Portanto passar somente o objeto req para o controller quando chamá-lo na HOF. Assim evitamos qualquer uso de res.send na nossa aplicação como um todo. E para responder as requests de sucesso podemos retornar um objeto com o statusCode e o body no controller.

Lembrando que as respostas de erro serão respondidas via throw new CustomError

Sendo assim, podemos adaptar os controllers para retornarem esse objeto com as informações da resposta. Então a nossa HOF recebe esse objeto retornado pela execução do controller que já possuirá todas informações para responder a request, e chama res.send para respondê-la.

Esse artigo não cita em momento algum testes para o código sugerido, mas testá-lo unitariamente não deveria ser um desafio muito grande.

Esse é um formato que cheguei depois de um tempo brincando em uma API onde aplico over-engineering para explorar novas formar de se resolver um problema. Você pode encontrar mais exemplos no repositório onde mantenho esse código.

--

--