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.
- Entendendo o que é uma exceção
- Try catch em ação
- Quando o try catch se torna um obstáculo
- Middleware de manipulação de erros
- Middleware como solução elegante
- Personalizando os erros
- Personalizando o middleware de erros
- Erros assíncronos
- Recomendações
- Conclusão
Entendendo o que é uma exceção
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());
});
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:
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:
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");
}
});
O usuário vai receber:
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);
}
});
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");
}
Status Code 404 (Not Found):
if (!livro) {
throw new Error("Livro não encontrado");
}
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");
}
}
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
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);
});
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 objetonext
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.
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);
});
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);
}
}
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!");
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 });
};
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);
}
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);
}
});
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
Em seguida, adicione no topo do seu arquivo index.ts
:
import "express-async-errors";
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)
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 🦤.
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 usoGrato, primo