Tratamento Global de Erros no Express: AppError, Zod e Sentry
Como eliminar try/catch dos controllers usando express-async-errors. Tratamento centralizado diferenciando AppError (cliente) de erros internos (servidor), validação com ZodError e integração com Sentry para logs silenciosos.
O antipadrão mais comum em APIs Node.js é o try/catch massivo em cada controller, repetindo a mesma lógica de formatar um JSON com status 500. Isso fere o DRY, infla os arquivos e frequentemente esconde erros críticos porque um catch engoliu a stack trace. A arquitetura correta delega todo o tratamento de erros para um único middleware global no fim do pipeline.
A Classe AppError (Erros Esperados)
Precisamos distinguir Erros Operacionais (erros que prevemos: e-mail duplicado, sem saldo, não autorizado) de Erros de Programação (TypeError, banco fora do ar). Criamos a classe AppError para representar falhas operacionais seguras para retornar ao cliente.
export class AppError extends Error {
public readonly statusCode: number;
public readonly isOperational: boolean;
constructor(message: string, statusCode = 400) {
super(message);
this.name = 'AppError';
this.statusCode = statusCode;
this.isOperational = true; // Flag para identificar erros seguros
// Garante que o stack trace seja preservado na classe correta
Error.captureStackTrace(this, this.constructor);
}
}O Problema das Promises no Express 4
O Express 4.x não captura exceções lançadas dentro de funções async. O erro vaza e a requisição fica presa (hanging) até o timeout do browser. A solução é o pacote express-async-errors (desnecessário no Express 5+).
npm install express-async-errorsimport express from 'express';
import 'express-async-errors'; // IMPORTANTE: logo no topo!
import { routes } from './routes';
import { errorHandler } from './middlewares/errorHandler';
const app = express();
app.use(express.json());
app.use(routes);
// O middleware de erro deve ser o ÚLTIMO a ser registrado
app.use(errorHandler);O Middleware Global (errorHandler)
O middleware de erro tem exatamente 4 parâmetros (err, req, res, next) — o Express identifica error handlers por essa assinatura específica. Com apenas 3 parâmetros, o Express trata o middleware como normal. A hierarquia de tratamento importa: AppError (negocial) → ZodError (validação) → erros inesperados (500). Cada tipo tem resposta e comportamento diferentes: AppErrors não vão ao Sentry (são esperados), erros 500 geram alertas imediatos com stack trace completo.
import type { Request, Response, NextFunction } from 'express';
import { ZodError } from 'zod';
import { AppError } from '../errors/AppError';
import { logger } from '../utils/logger'; // Seu Winston ou Pino
export function errorHandler(
err: Error,
req: Request,
res: Response,
_next: NextFunction
): Response {
// 1. Erros Operacionais (Regra de Negócio)
if (err instanceof AppError) {
return res.status(err.statusCode).json({
status: 'error',
message: err.message,
});
}
// 2. Erros de Validação (Zod)
if (err instanceof ZodError) {
const errors = err.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
}));
return res.status(422).json({
status: 'error',
message: 'Validation failed',
details: errors,
});
}
// 3. Erros Críticos de Servidor (Programação / Infra)
// Apenas estes devem ser logados com stack trace e enviados ao Sentry
logger.error({
message: err.message,
stack: err.stack,
method: req.method,
path: req.path,
body: req.body, // cuidado com dados sensíveis aqui!
});
// Nunca vaze mensagens de erro de banco de dados para o cliente em produção!
const message = process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message;
return res.status(500).json({
status: 'error',
message,
});
}Uso Limpo no Use Case
import { AppError } from '../errors/AppError';
export class WithdrawMoneyUseCase {
async execute(userId: string, amount: number) {
const user = await this.usersRepo.findById(userId);
if (!user) {
// O fluxo é abortado imediatamente, sem try/catch.
// O errorHandler formata o 404 automaticamente.
throw new AppError('Usuário não encontrado.', 404);
}
if (user.balance < amount) {
throw new AppError('Saldo insuficiente.', 422);
}
// ...
}
}Integração com Sentry/Datadog: No bloco de erros críticos (500) do middleware, você deve disparar alertas. Nunca mande alertas para AppErrors (ex: 401 Unauthorized), pois um ataque de bot de login lotará a cota do Sentry de erros falsos.
Conclusão
O express-async-errors é um monkey-patch que sobrescreve o handler de requisições do Express para envolver automaticamente funções async em um try/catch que chama next(err). No Express 5 (release candidate) esse comportamento já é nativo. A classificação isOperational: true na AppError serve como flag para sistemas de monitoramento: apenas erros com isOperational: false (ou ausente) são erros de programação que merecem alertas de plantão — erros de usuário como "senha incorreta" nunca deveriam aparecer no seu PagerDuty.
Delegar os erros para a camada externa (middleware) limpa a lógica interna. O Use Case apenas lança o AppError e encerra a execução. O cliente recebe sempre uma resposta JSON previsível. A equipe de monitoramento (Sentry) recebe apenas logs acionáveis de falhas reais do sistema, sem ruído de erros operacionais de usuários.