Tratamento de erros no Express

1. Fundamentos do tratamento de erros no Express

1.1. Middleware de erro: assinatura e funcionamento

No Express, middlewares de erro são funções com quatro parâmetroserr, req, res, next. O primeiro parâmetro é obrigatoriamente o erro, diferentemente de middlewares comuns que possuem apenas três parâmetros.

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ message: 'Erro interno do servidor' });
});

1.2. Como o Express identifica e propaga erros

Quando você chama next(err) dentro de qualquer middleware ou rota, o Express pula todos os middlewares comuns seguintes e procura o próximo middleware de erro na cadeia.

app.get('/dados', (req, res, next) => {
  const dados = obterDados();
  if (!dados) {
    const erro = new Error('Dados não encontrados');
    erro.statusCode = 404;
    next(erro); // Propaga para o middleware de erro
    return;
  }
  res.json(dados);
});

1.3. Diferença entre middleware comum e middleware de erro

  • Middleware comum: 3 parâmetros (req, res, next) — processa requisições normais.
  • Middleware de erro: 4 parâmetros (err, req, res, next) — captura erros propagados via next(err) ou exceções lançadas.

Se um middleware comum lançar uma exceção não capturada, o Express automaticamente a encaminha para o middleware de erro.

2. Estratégias para capturar erros em rotas assíncronas

2.1. Try/catch manual em handlers async

app.get('/usuarios/:id', async (req, res, next) => {
  try {
    const usuario = await buscarUsuario(req.params.id);
    res.json(usuario);
  } catch (err) {
    next(err);
  }
});

2.2. Wrapper function para evitar repetição de try/catch

Crie uma função utilitária que envolve handlers assíncronos:

const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/usuarios/:id', asyncHandler(async (req, res) => {
  const usuario = await buscarUsuario(req.params.id);
  res.json(usuario);
}));

2.3. Utilizando o pacote express-async-errors

Este pacote faz o Express capturar automaticamente rejeições de promises em rotas assíncronas:

import 'express-async-errors';

app.get('/usuarios/:id', async (req, res) => {
  const usuario = await buscarUsuario(req.params.id); // Erros aqui são capturados automaticamente
  res.json(usuario);
});

3. Criando uma classe de erro personalizada

3.1. Estendendo a classe Error nativa do JavaScript

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true; // Indica que é um erro operacional esperado
    Error.captureStackTrace(this, this.constructor);
  }
}

3.2. Adicionando propriedades: statusCode, isOperational

  • statusCode: código HTTP apropriado (400, 404, 500, etc.)
  • isOperational: distingue erros operacionais (esperados) de erros de programação (inesperados)

3.3. Exemplo prático: AppError para erros de negócio

app.post('/transferencia', async (req, res, next) => {
  try {
    const { contaOrigem, contaDestino, valor } = req.body;

    if (valor <= 0) {
      throw new AppError('Valor da transferência deve ser positivo', 400);
    }

    const saldo = await obterSaldo(contaOrigem);
    if (saldo < valor) {
      throw new AppError('Saldo insuficiente', 400);
    }

    await realizarTransferencia(contaOrigem, contaDestino, valor);
    res.json({ message: 'Transferência realizada com sucesso' });
  } catch (err) {
    next(err);
  }
});

4. Middleware global de tratamento de erros

4.1. Estrutura do middleware de erro centralizado

app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const message = err.message || 'Erro interno do servidor';

  res.status(statusCode).json({
    status: 'error',
    statusCode,
    message
  });
});

4.2. Como diferenciar erros operacionais de erros de programação

app.use((err, req, res, next) => {
  if (err.isOperational) {
    // Erro esperado: retorna mensagem amigável
    res.status(err.statusCode).json({
      status: 'error',
      message: err.message
    });
  } else {
    // Erro de programação: log detalhado, resposta genérica
    console.error('ERRO NÃO OPERACIONAL:', err);
    res.status(500).json({
      status: 'error',
      message: 'Algo deu errado. Tente novamente mais tarde.'
    });
  }
});

4.3. Envio de respostas JSON padronizadas para o cliente

app.use((err, req, res, next) => {
  const response = {
    success: false,
    error: {
      code: err.statusCode || 500,
      message: err.message || 'Internal Server Error'
    }
  };

  // Em desenvolvimento, inclui stack trace
  if (process.env.NODE_ENV === 'development') {
    response.error.stack = err.stack;
  }

  res.status(response.error.code).json(response);
});

5. Tratamento de erros específicos do Express

5.1. Erro 404 para rotas não encontradas

// Deve ser o último middleware antes do middleware de erro
app.use((req, res, next) => {
  next(new AppError(`Rota ${req.originalUrl} não encontrada`, 404));
});

5.2. Erros de validação de entrada (ex: Joi, express-validator)

const { validationResult } = require('express-validator');

app.post('/usuario', [
  body('email').isEmail().withMessage('Email inválido'),
  body('senha').isLength({ min: 6 }).withMessage('Senha deve ter no mínimo 6 caracteres')
], (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return next(new AppError(errors.array().map(e => e.msg).join(', '), 400));
  }
  // Processa a requisição...
});

5.3. Erros de parsing (JSON malformado, payload muito grande)

// JSON malformado
app.use((err, req, res, next) => {
  if (err.type === 'entity.parse.failed') {
    return res.status(400).json({ message: 'JSON inválido no corpo da requisição' });
  }
  next(err);
});

// Payload muito grande (limite padrão: 100kb)
app.use(express.json({ limit: '10mb' }));
app.use((err, req, res, next) => {
  if (err.type === 'entity.too.large') {
    return res.status(413).json({ message: 'Payload muito grande' });
  }
  next(err);
});

6. Integração com logging e monitoramento

6.1. Logging estruturado com Winston no middleware de erro

const winston = require('winston');

const logger = winston.createLogger({
  level: 'error',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log' }),
    new winston.transports.Console({ format: winston.format.simple() })
  ]
});

app.use((err, req, res, next) => {
  logger.error({
    message: err.message,
    stack: err.stack,
    method: req.method,
    url: req.originalUrl,
    timestamp: new Date().toISOString()
  });
  next(err);
});

6.2. Diferença entre log de desenvolvimento e produção

if (process.env.NODE_ENV === 'production') {
  logger.add(new winston.transports.File({ filename: 'errors.log' }));
} else {
  logger.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    )
  }));
}

6.3. Envio de erros para serviços externos (Sentry)

const Sentry = require('@sentry/node');
Sentry.init({ dsn: process.env.SENTRY_DSN });

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.errorHandler());

app.use((err, req, res, next) => {
  Sentry.captureException(err);
  res.status(500).json({ message: 'Erro reportado à equipe de desenvolvimento' });
});

7. Tratamento de erros no frontend React

7.1. Interceptando respostas de erro da API com Axios interceptors

import axios from 'axios';

const api = axios.create({
  baseURL: 'http://localhost:3000/api'
});

api.interceptors.response.use(
  response => response,
  error => {
    if (error.response) {
      const { status, data } = error.response;

      switch (status) {
        case 400:
          console.error('Erro de validação:', data.message);
          break;
        case 401:
          console.error('Não autorizado');
          // Redirecionar para login
          break;
        case 404:
          console.error('Recurso não encontrado');
          break;
        case 500:
          console.error('Erro interno do servidor');
          break;
        default:
          console.error('Erro desconhecido');
      }
    }
    return Promise.reject(error);
  }
);

7.2. Exibindo mensagens de erro amigáveis ao usuário

import React, { useState } from 'react';

function LoginForm() {
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      await api.post('/login', { email, senha });
      // Sucesso: redirecionar
    } catch (err) {
      const message = err.response?.data?.message || 'Erro de conexão. Tente novamente.';
      setError(message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error-message">{error}</div>}
      {/* Campos do formulário */}
      <button type="submit" disabled={loading}>
        {loading ? 'Carregando...' : 'Entrar'}
      </button>
    </form>
  );
}

7.3. Criando um componente de fallback com Error Boundaries

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Erro capturado pelo Error Boundary:', error, errorInfo);
    // Enviar para serviço de monitoramento
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Algo deu errado</h2>
          <p>Desculpe pelo inconveniente. Tente recarregar a página.</p>
          <button onClick={() => window.location.reload()}>
            Recarregar
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Uso:
function App() {
  return (
    <ErrorBoundary>
      <MainContent />
    </ErrorBoundary>
  );
}

Referências