Voltar para Artigos
Back-end6 min de leitura

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.

12 de agosto de 2026

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.

AppError.ts
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+).

bash
npm install express-async-errors
server.ts
import 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.

errorHandler.ts
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

WithdrawMoneyUseCase.ts
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.