DEV Community

Cover image for Lidando com exceções: como fazer erros práticos e elegantes no Express js
Clinton Rocha
Clinton Rocha

Posted on

Lidando com exceções: como fazer erros práticos e elegantes no Express js

Para você, desenvolvedor(a) que esta construindo sua API no Express e se questiona sobre maneiras de aprimorar o tratamento de erros, este artigo promete demonstrar práticas comuns adotadas por muitos e os desafios que podem surgir. Mas a história não para por aí! Vou te apresentar uma abordagem para melhorar a qualidade e personalizar o tratamento de erros na sua aplicação.

um giff com varios erros na tela

Entendendo o que é uma exceção

gif error

Na programação, uma exceção é um evento ou condição que acontece durante a execução de um programa e que pode parar o fluxo da aplicação. O objetivo de lidar com exceções é garantir que o programa possa reagir da melhor maneira a esses eventos anormais, em vez de simplesmente quebrar ou encerrar com seu programa.

import express, { NextFunction, Request, Response } from "express";
import fs from "fs";

const app = express();

app.use(express.json());

app.use("/", (req: Request, res: Response) => {
  const file = fs.readFileSync("naoExiste.txt");
  return res.json(file.toString());
});
Enter fullscreen mode Exit fullscreen mode

Temos um código simples que tenta ler um arquivo txt quando o usuário acessa a rota /, você sabe o que acontece quando o usuário acessa essa rota sendo que o arquivo não existe? Isso mesmo, um erro 😯 e dos feios, o usuário recebe:

Erro dizendo que o arquivo não existe

Não para por aí, para piorar sua situação a aplicação para de funcionar e nenhum usuário vai conseguir acessar ela porque seu código não tem tratamento de erro:

Um erro no console dizendo que o arquivo não foi encontrado

No seu console o erro vai se maior que isso...

Entender e lidar com exceções é crucial para garantir a estabilidade do programa. No exemplo acima, a falta de tratamento de exceções resulta em mensagens de erro desagradáveis para o usuário e na interrupção da aplicação.

Try catch em ação

No JavaScript, try catch é uma estrutura usada para lidar com exceções (erros) durante a execução do código. O bloco try envolve o código que pode gerar uma exceção, enquanto o bloco catch é usado para lidar com a exceção se ela ocorrer. Vamos por o try catch em ação:

app.use("/", (req: Request, res: Response) => {
  try {
    const file = fs.readFileSync("naoExiste.txt");
    return res.json(file.toString());
  } catch (error) {
    return res.status(404).json("Arquivo não encontrado");
  }
});
Enter fullscreen mode Exit fullscreen mode

O usuário vai receber:

Um erro com a mensagem "parece que arquivo não existe..."

Bom, agora podemos tratar nossos erros, e note que nossa aplicação não caiu, isso é um avanço. Usar try catch é uma prática comum para tornar o código mais robusto e controlar erros de maneira elegante, em vez de permitir que o programa pare de funcionar quando ocorre uma exceção.

Quando o try catch se torna um obstáculo

O problema que quero destacar neste momento está relacionada à quantidade de vezes que é necessário usar a estrutura try-catch em uma simples aplicação. A implementação apresentada mostra a repetição do bloco try-catch em cada endpoint, o que pode tornar o código menos eficiente e mais suscetível a erros de manutenção quando sua aplicação for crescer.

//No mundo real essa variável 'livros' provavelmente seria uma interação com banco de dados
const livros = [
  { id: 1, titulo: "Livro 1", autor: "Autor 1" },
  { id: 2, titulo: "Livro 2", autor: "Autor 2" },
];

app.get("/livros", (req: Request, res: Response) => {
  try {
      if(!livros){
          throw new Error("Nenhum Livro encontrado");
      }
    res.status(200).json(livros);
  } catch (error) {
        res.status(404).json(error.message);
  }
});

app.get("/livros/:id", (req: Request, res: Response) => {
  try {
      const livroId = parseInt(req.params.id);
      if(!livroId){
          throw new Error("ID Invalido");
      }
      const livro = livros.find((l) => l.id === livroId);
      if (!livro) {
        throw new Error("Livro não encontrado");
      }
      res.status(200).json(livro);
  } catch (error) {
      res.status(404).json(error.message);
  }
});

app.post("/livros", (req: Request, res: Response) => {
    try {
        const novoLivro = req.body;
        if(!novoLivro){
            throw new Error("Informações invalida");
        }
        livros.push(novoLivro);
        res.status(201).json("Livro adicionado com sucesso");
    } catch (error) {
        res.status(500).json(error.menssage);
    }
});
Enter fullscreen mode Exit fullscreen mode

um diagrama representando a rota

Podemos ver a repetição que é usar essa estrutura para lidar com as exceções, mas não é só isso, existem momentos em que é necessário lidar com peculiaridades dessa estrutura, por exemplo, no exemplo anterior existe uma rota que pode gerar 3 tipos de status:

Status Code 400 (Bad Request):

if(!livroId){
  throw new Error("ID Invalido");
}
Enter fullscreen mode Exit fullscreen mode

Status Code 404 (Not Found):

if (!livro) {
    throw new Error("Livro não encontrado");
}
Enter fullscreen mode Exit fullscreen mode

Até daria para fazer um amontoado de if e else como podemos ver em:

catch (error) { 
    if (error.message === "ID Inválido") {
        res.status(400).json(error.message);
    } else if (error.message === "Livro não encontrado"){
        res.status(404).json(error.message);
    } else { 
        res.status(500).json("Erro interno do servidor"); 
    } 
}
Enter fullscreen mode Exit fullscreen mode

Mas convenhamos que isso deixa o código um pouco poluído e dificulta consequentemente a leitura e futuras manutenções. É importante considerar alternativas que permitam manter a manipulação de erros de forma eficaz, sem a sobrecarga de código.

Middleware de manipulação de erros

Na imagem demonstra o lugar que o Middleware ''fica'' que é entre o client e o servidor

Vamos explorar uma solução para o nosso problema em específico, visando simplificar a estrutura, melhorar a legibilidade e facilitar futuras modificações na API.

Deixa eu te apresentar nosso amigo Middleware, ele tem acesso ao objeto de solicitação (req), o objeto de resposta (res) e com isso podemos solucionar os problemas que foram citados anteriormente, esse colega é atua entre o usuário e sua aplicação, podemos usar ele em situações diversas, no nosso caso vamos usá-lo na manipulação de erros.

app.use((error: Error, req: Request, res: Response, next: NextFunction)=> {
    //aqui que entra a magica =D
    return res.json(error.message);
});
Enter fullscreen mode Exit fullscreen mode

Sempre defina os middlewares de manipulação de erros por último, após outros app.use() e camadas de rota.

Middlewares de manipulação de erros sempre levam quatro argumentos. Você deve fornecer quatro argumentos para identificá-lo como uma função de middleware de manipulação de erros. Mesmo se você não precisar usar o objeto next, você deve especificá-lo para manter a assinatura. Caso contrário, o objeto next será interpretado como um middleware comum e a manipulação de erros falhará.

Essa é a estrutura básica de um Middleware no Express especificamente para manipulação de erros.

diagrama monstrando as rotas com o uso do middleware

Como ilustrado acima, o Middleware de erro atua entre o usuário e as rotas. Tente ver ele como o responsável por lidar com problemas. Qualquer exceção ocorrida em qualquer camada da aplicação pode ser interceptada por esse middleware. Ele desempenha um papel crucial ao capturar e tratar erros, garantindo uma resposta apropriada ao usuário e facilitando a identificação e resolução de problemas na aplicação.

Middleware como solução elegante

Agora que você compreendeu o funcionamento do middleware e reconheceu sua importância na prevenção de falhas, surge a pergunta: "Devo deixar de usar o try-catch?"

Calma, pequeno Padawan. O try-catch tem seus problemas como foi citado ao decorrer desse conteúdo, essa estrutura é bastante útil em aplicações de pequeno porte, é necessário cogitar a retirada dessa estrutura quando sua aplicação começa a crescer e ter um aumento no nível de complexidade, é aqui que nosso aliado, o Middleware, entra em cena para simplificar e aprimorar a manipulação de erros.

Padawan significa aprendiz , iniciante , e é um termo utilizado nos filmes Star Wars.

No exemplo a seguir apresentado, utilizamos o Middleware para lidar com erros de forma centralizada. Ao invés de repetir a estrutura try-catch em cada rota, delegamos essa responsabilidade ao Middleware de erros.

app.get("/livros", (req: Request, res: Response) => {
  if(!livros){
      throw new Error("Não existe nenhum livro");
  }
      res.status(200).json(livros);
});

app.get("/livros/:id", (req: Request, res: Response) => {
  const livroId = parseInt(req.params.id);
  if (!livroId) {
    throw new Error("ID invalido")
  }
  const livro = livros.find((l) => l.id === livroId);
  if (!livro) {
    throw new Error("O livro que você procura não existe")
  }
  res.status(200).json(livro);
});

app.post("/livros", (req: Request, res: Response) => {
  const novoLivro = req.body; 
  if(!novoLivro){
      throw new Error("Informe um livro valido");
  }
   livros.push(novoLivro);
   res.status(201).json("Livro adicionado com sucesso");
});

app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
    //aqui que entra a magica =D
    return res.status(400).json(error.message);
});
Enter fullscreen mode Exit fullscreen mode

O resultado é notável, o middleware intercepta esses erros, evitando a necessidade de try-catch em cada rota. Além disso, conseguimos personalizar as respostas de erro de maneira mais controlada e esteticamente agradável.

Você deve ter notado que não conseguimos mudar o status code, mas não se preocupe ainda vamos aprimorar esse código, ajustando os códigos de status e refinando as respostas de erro. O importante é que sua aplicação não falha catastroficamente, e agora, estamos trilhando um caminho mais elegante na gestão de exceções.

Personalizando os erros

Você deve saber que a quantidade de status code é limitada, e isso pode diminuir muito mais ao depender da sua aplicação, tendo isso em mente, vamos usar o principio Princípio da responsabilidade única do SOLID para melhorar a legibilidade do nosso código e consequente deixando mais organizado.

export class ApiError extends Error {
  public readonly statusCode: number;

  constructor(message: string, statusCode: number) {
    super(message);
    this.statusCode = statusCode;
  }
}

export class BadRequestError extends ApiError {
  constructor(message: string) {
    super(message, 400);
  }
}

export class NotFoundError extends ApiError {
  constructor(message: string) {
    super(message, 404);
  }
}

export class UnauthorizedError extends ApiError {
  constructor(message: string) {
    super(message, 401);
  }
}
Enter fullscreen mode Exit fullscreen mode

Vamos usar a classe ApiError como classe base que estende a classe nativa Error e adicionar statusCode. No tópico acima, ao falar do middleware foi dito que não dava para mudar o status code do erro ao usar throw new Error(), mas com essa implementação podemos ver que esse problema foi solucionado.

throw new BadRequestError("ID Invalido");

throw new NotFoundError("Livro não encontrado");

throw new UnauthorizedError("Não autorizado!");
Enter fullscreen mode Exit fullscreen mode

Personalizando o middleware de erros

Ao implementar as melhorias no lançamento de erros é necessário melhorar o responsável por lidar com esses erros, confesso que não tem muito segredo.

import { NextFunction, Request, Response } from "express";
import { ApiError } from "./helpers/api-errors";

export const errorMiddleware = (
  error: Error & Partial<ApiError>,
  request: Request,
  response: Response,
  next: NextFunction
) => {
  const statusCode = error.statusCode ?? 500;
  const message = error.statusCode ? error.message : "Internal Server Error";

  return response.status(statusCode).json({ message });
};
Enter fullscreen mode Exit fullscreen mode

Acredito que a parte que você precisa ter cuidado para não se esquecer é em relação ao type do parâmetro error que é Error & Partial<ApiError> lembre-se que statusCode não existe no type nativo Error, sendo necessário fazer essa intersecção e o uso do Partial<> do Typescript porque nem sempre o erro lançado vai ser gerado pelo seu código, às vezes pode ser algo relacionado ao baco de dados ou algum outro problema.

Erros assíncronos

Legal, já melhoramos nosso código e estamos preparados para todo erro que aparecer, será mesmo? Todos os exemplos utilizados até agora foram códigos síncronos e nenhum nenhum assíncrono.

app.get("/livros", async (req: Request, res: Response) => { 
    const livros = await buscarLivrosDoBancoDeDados(); 
    if (!livros) { 
     throw new NotFoundError("Livro não encontrado"); 
    } 
    res.status(200).json(livros); 
}
Enter fullscreen mode Exit fullscreen mode

Se ocorrer algum problema no banco de dados no exemplo acima, seu middleware seria incapaz de capturar o erro, resultando na quebra da aplicação.

app.get("/livros", async (req: Request, res: Response, next: NextFunction) => { 
    try { 
        const livros = await buscarLivrosDoBancoDeDados(); 
        if (!livros) { 
            throw new NotFoundError("Livro não encontrado"); 
        } 
        res.status(200).json(livros); 
        } catch (error) { 
            next(error); 
        } 
});
Enter fullscreen mode Exit fullscreen mode

Para que nosso middleware funcionasse teríamos que voltar a usar o try catch em conjunto do next(error) e só assim o Express conseguiria ter acesso a esse erro, mas não se preocupe que existe uma solução para isso.

A lib express-async-errors oferece um suporte simples e eficaz para lidar com funções assíncronas utilizando ES6 async/await no Express. O principal objetivo é simplificar a manipulação de erros em funções assíncronas, eliminando a necessidade de chamar manualmente a função next para propagar erros.

npm i express-async-errors
Enter fullscreen mode Exit fullscreen mode

Em seguida, adicione no topo do seu arquivo index.ts:

import "express-async-errors";
Enter fullscreen mode Exit fullscreen mode

Pronto, agora podemos voltar a lançar os erros sem o uso do try catch. Essencialmente, a lib express-async-errors simplifica a manipulação de erros em funções assíncronas no Express, permitindo que você use exceções para tratar erros sem a necessidade de chamar explicitamente next.

Recomendações

Todos os links que foram utilizados nesse artigo são bastante uteis para sua compreensão, não pule nenhum. Fora isso eu tenho algumas recomendações: 

Daniel Reis com sua didática excepcional fez o artigo Criando Exceptions para impressionar no Teste Técnico a abordagem dele é parecida com a usada nesse artigo, provavelmente você vai conseguir melhorar ainda mais seu código ao ler esse artigo.

Miguel Barbosa tem um artigo muito maneiro com tratamento de erro, Como tratar erros http no Spring Boot , eu nunca fiz algo em Java, mas a explicação é tão legal e com uma abordagem super compreensível que chega a ser impossível você não entender, recomendo.

Erick Wendel tem um vídeo muito maneiro sobre erros no Javascript Sistemas feitos em JavaScript não são confiáveis?! Error Handling lá ele fala um pouco sobre o try catch e muito mais.

Conclusão

Esse conteúdo foi mais complicado do que imaginei, espero que você leitor tenha entendido e aprendido algo novo. No casso de algum erro no código ou falha na explicação eu ficarei grato com seu feedback, você consegue me encontrar no twitter ou no Discord da He4rt.

Obrigado por ler até aqui.

Top comments (3)

Collapse
 
raulferreirasilva profile image
Raul Ferreira

Cara muito obrigado por compartilhar seu conhecimento, não conhecia Middleware, achei fenomenal sua didática e as imagens me ajudaram bastante no entendimento.
Sempre usei o try catch achei que tivesse uma maneira de melhorar, mas nunca corri atras, vou tentar aplicar em alguns projetos pequenos só para treinar e aprimorar meu conhecimento, muito grato 🦤.

Collapse
 
cherryramatis profile image
Cherry Ramatis

Acompanhar a criação desse conteúdo incrível foi uma honra! didática impressionante que me ensinou pra KRL.

Não conhecia express-async-errors, mto massa ver os casos de uso

Collapse
 
phenriquesousa profile image
Pedro Henrique

Grato, primo