Tratamento de erro na prática: além do try/catch

Por que espalhar try/catch em todo lugar não resolve tratamento de erro de verdade. Entenda classes de erro customizadas, a diferença entre erro esperado e inesperado, e tratamento centralizado numa API.


try/catch é ensinado cedo, mas raramente é ensinado bem. A maioria aprende a sintaxe e sai espalhando esse bloco em qualquer lugar que pode falhar, sem pensar no que fazer de fato com o erro capturado. O resultado comum é um catch vazio, ou um console.log(erro) que não ajuda ninguém a entender o que aconteceu depois, em produção, longe do terminal de quem escreveu o código.

O problema do catch genérico

try {
  await processarPagamento(pedido);
} catch (erro) {
  console.log("deu erro");
}

Esse código captura qualquer coisa que der errado, sem distinguir os casos. Um cartão recusado, uma falha de conexão com o gateway de pagamento e um bug no próprio código são tratados exatamente da mesma forma: um log genérico, sem contexto nenhum sobre qual dos três realmente aconteceu, nem sobre o que fazer a seguir em cada caso.

Erro esperado x erro inesperado

A distinção que mais ajuda a organizar tratamento de erro é separar dois tipos. Um erro esperado é uma condição que já faz parte do domínio do problema: cartão recusado, e-mail já cadastrado, estoque insuficiente. Esses casos não são bug, são parte normal do fluxo, e o sistema deveria saber lidar com eles de forma específica, geralmente devolvendo uma mensagem clara pro usuário.

Um erro inesperado é uma falha de verdade: uma exceção de null, uma conexão que caiu no meio, um bug que ninguém previu. Esses casos exigem outro tipo de resposta, geralmente um log detalhado pra investigação e uma mensagem genérica pro usuário, sem expor detalhe técnico interno que não ajuda ele em nada.

Classes de erro customizadas

Criar classes de erro específicas pro seu domínio resolve boa parte dessa confusão, porque o tipo do erro já carrega informação sobre o que aconteceu:

class ErroDominio extends Error {
  constructor(mensagem, codigo) {
    super(mensagem);
    this.codigo = codigo;
    this.name = this.constructor.name;
  }
}
 
class CartaoRecusado extends ErroDominio {
  constructor() {
    super("Cartão recusado pela operadora", "CARTAO_RECUSADO");
  }
}
 
class EstoqueInsuficiente extends ErroDominio {
  constructor(produtoId) {
    super(`Estoque insuficiente para o produto ${produtoId}`, "ESTOQUE_INSUFICIENTE");
    this.produtoId = produtoId;
  }
}

Com isso, a função que processa o pedido lança o erro específico que representa o que realmente aconteceu:

async function processarPagamento(pedido) {
  const resultado = await gateway.cobrar(pedido);
  if (resultado.recusado) {
    throw new CartaoRecusado();
  }
  return resultado;
}

E quem captura esse erro consegue decidir o que fazer baseado no tipo dele, não num texto genérico:

try {
  await processarPagamento(pedido);
} catch (erro) {
  if (erro instanceof CartaoRecusado) {
    return res.status(402).json({ erro: erro.message, codigo: erro.codigo });
  }
  if (erro instanceof EstoqueInsuficiente) {
    return res.status(409).json({ erro: erro.message, codigo: erro.codigo });
  }
  throw erro; // erro inesperado, deixa subir pro tratamento central
}

Tratamento centralizado numa API

Espalhar try/catch em toda rota, repetindo a mesma lógica de resposta de erro em cada uma, cria duplicação e inconsistência: uma rota devolve { erro: "..." }, outra devolve { message: "..." }, e cada desenvolvedor formata do próprio jeito. O caminho mais limpo é deixar o erro inesperado subir, sem capturar em toda rota individualmente, e tratar ele num middleware central:

app.use((erro, req, res, next) => {
  if (erro instanceof ErroDominio) {
    return res.status(erro.status ?? 400).json({
      erro: erro.message,
      codigo: erro.codigo,
    });
  }
 
  console.error("[erro não tratado]", erro);
  return res.status(500).json({ erro: "Algo deu errado, tenta de novo" });
});

Isso garante um formato de resposta consistente pra qualquer erro do domínio, em qualquer rota, e centraliza o log de erro inesperado num único lugar, em vez de duplicado (ou esquecido) em cada catch espalhado pelo código.

Não engolir o erro

O pecado mais comum de tratamento de erro malfeito é o catch vazio, ou que só loga e segue como se nada tivesse acontecido:

// nunca faça isso
try {
  await enviarNotificacao(usuario);
} catch {
  // silêncio total
}

Se o erro realmente não importa pro fluxo continuar (por exemplo, uma notificação que pode falhar sem comprometer o resto do processo), isso deveria ser uma decisão explícita, registrada com um log, não um silêncio completo que esconde qualquer problema até alguém notar de outro jeito, geralmente reclamação de usuário.

Fechando

Tratamento de erro de verdade não é sobre colocar try/catch em tudo, é sobre diferenciar o que é esperado do que não é, dar nome específico pro que pode dar errado, e decidir de forma consciente o que fazer com cada caso, em vez de um catch genérico que trata tudo igual e não ajuda ninguém a entender o que aconteceu depois.

Direto na sua
caixa de entrada.

Um aviso por e-mail sempre que eu publicar um post novo. Sem spam, sem newsletter chata, só isso.