Lidando com exceções: como fazer erros práticos e elegantes no Express js
Tags: braziliandevs, webdev, javascript, programming
Published: 2024-01-16 00:00:29

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. 

<img src="https://media.giphy.com/media/Q0OqU2N1MN3KU/giphy.gif" alt="um giff com varios erros na tela"/>

- [Entendendo o que é uma exceção](#entendendo-o-que-é-uma-exceção)
- [Try catch em ação](#try-catch-em-ação)
- [Quando o try catch se torna um obstáculo](#quando-o-try-catch-se-torna-um-obstáculo)
- [Middleware de manipulação de erros](#middleware-de-manipulação-de-erros)
- [Middleware como solução elegante](#middleware-como-solução-elegante)
- [Personalizando os erros](#personalizando-os-erros)
- [Personalizando o middleware de erros](#personalizando-o-middleware-de-erros)
- [Erros assíncronos ](#erros-assíncronos)
- [Recomendações](#recomendações)
- [Conclusão](#conclusão)

## Entendendo o que é uma exceção

<img src="https://media.giphy.com/media/bi6RQ5x3tqoSI/giphy.gif" alt="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.

```typescript
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: 

![Erro dizendo que o arquivo não existe](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9hxrsb1uf1wm7jddzh4n.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/izcl6jd5zwj4m5ctop78.png)
>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](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Statements/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](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Statements/try...catch) em ação:

```ts
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:

![Um erro com a mensagem "parece que arquivo não existe..."](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zug33f5080m75db38u4t.png)

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.

```ts
//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);
	}
});
```

![um diagrama representando a rota](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4dxij932zjyqpby9wq82.png)

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)**:
```typescript
if(!livroId){
  throw new Error("ID Invalido");
}
```

**Status Code 404 (Not Found):**
```typescript
if (!livro) {
	throw new Error("Livro não encontrado");
}
```

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

```typescript
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

![Na imagem demonstra o lugar que o Middleware ''fica'' que é entre o client e o servidor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k71za3kimgvp2d6rbnov.png)

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](http://expressjs.com/pt-br/guide/using-middleware.html), ele tem acesso ao [objeto de solicitação](http://expressjs.com/pt-br/4x/api.html#req) (`req`), o [objeto de resposta](http://expressjs.com/pt-br/4x/api.html#res) (`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.

```typescript
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 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](http://expressjs.com/pt-br/guide/error-handling.html).

![diagrama monstrando as rotas com o uso do middleware](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17uenxm9bb702n3hjk4k.png)

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. 

```typescript
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](https://http.cat) é 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](https://dev.to/clintonrocha98/era-solid-o-que-me-faltava-bhp#s-princípio-da-responsabilidade-única) do SOLID para melhorar a legibilidade do nosso código e consequente deixando mais organizado.

```typescript
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.

```typescript
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.   

```typescript
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](https://developer.mozilla.org/pt-BR/docs/Glossary/Synchronous) e nenhum nenhum [assíncrono](https://developer.mozilla.org/pt-BR/docs/Glossary/Asynchronous).

```typescript
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.

```typescript
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.

```bash
npm i express-async-errors
```

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

```typescript
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](https://dev.to/danielhe4rt) com sua didática excepcional fez o artigo [Criando Exceptions para impressionar no Teste Técnico](https://dev.to/he4rt/criando-exceptions-para-impressionar-no-teste-tecnico-2nie) 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](https://dev.to/m1guelsb) tem um artigo muito maneiro com tratamento de erro, [Como tratar erros http no Spring Boot](https://dev.to/m1guelsb/como-tratar-erros-http-no-spring-boot-41de) , 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](https://twitter.com/erickwendel_) tem um vídeo muito maneiro sobre erros no Javascript [Sistemas feitos em JavaScript não são confiáveis?! Error Handling](https://youtu.be/iC_tKAyLeag?si=6WxhIjYFx03FpALg) 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](https://twitter.com/ClintonRocha_) ou no Discord da [He4rt](https://discord.gg/he4rt).

Obrigado por ler até aqui.