Tratamento de Erros: Exceções, Result Types e Fail Fast
Princípios fundamentais de confiabilidade de software: o custo das exceções, modelagem explícita com o padrão Result e tratamento com respostas padronizadas via Problem Details.
Neste artigo
O mecanismo de exceções (try-catch-throw) foi projetado na década de 1980 para tratar eventos excepcionais de hardware e ambiente: disco esgotado, perda abrupta de conexão de rede ou falha de alocação de memória. No entanto, muitos sistemas adotaram o antipadrão de lançar exceções para fluxos de negócio previsíveis, como um usuário não encontrado ou um cupom de desconto expirado.
flowchart TD
subgraph ExcecaoFluxo["Antipadrão: Exceção como Controle de Fluxo"]
ReqA["Cliente envia Pedido"]
ValidaA{"Cupom Válido?"}
ThrowEx["throw CupomExpiradoException()<br/>(Coleta stack trace cara)"]
CatchA["catch (Exception ex) genérico"]
ReqA --> ValidaA
ValidaA -- "Não" --> ThrowEx --> CatchA
end
subgraph ResultPattern["Padrão Recomendado: Tipo Result Explícito"]
ReqB["Cliente envia Pedido"]
ValidaB{"Cupom Válido?"}
RetOk["return Result.Success(pedido)"]
RetFail["return Result.Failure(ErroNegocio.CupomExpirado)"]
ReqB --> ValidaB
ValidaB -- "Sim" --> RetOk
ValidaB -- "Não" --> RetFail
end
O Alto Custo Oculto das Exceções
Quando o runtime dispara uma exceção, ele é forçado a pausar a execução e percorrer toda a árvore da pilha de chamadas (stack unwinding), capturando números de linhas, referências a métodos e metadados de depuração. Esse processo custa ordens de magnitude a mais do que um simples retorno de método condicional. Em cenários de alta concorrência com milhares de requisições por segundo, lançar exceções em cenários esperados satura a CPU e derruba o throughput do serviço.
O Padrão Result: Modelagem Explícita
O padrão Result (popularizado por linguagens como Rust e F# e amplamente aplicado em C# e TypeScript) transforma o resultado de uma operação em um contrato tipado que pode representar sucesso ou falha:
public readonly struct Result<TValue>
{
public bool IsSuccess { get; }
public TValue Value { get; }
public Error Error { get; }
private Result(TValue value)
{
IsSuccess = true;
Value = value;
Error = Error.None;
}
private Result(Error error)
{
IsSuccess = false;
Value = default!;
Error = error;
}
public static Result<TValue> Success(TValue value) => new(value);
public static Result<TValue> Failure(Error error) => new(error);
}
A assinatura do método expõe abertamente sua probabilidade de falha: Result<Usuario> ObterPorId(Guid id). O desenvolvedor é forçado a verificar result.IsSuccess antes de acessar result.Value, eliminando exceções do tipo NullReferenceException.
O Princípio Fail Fast e a RFC 7807
Quando uma falha realmente irrecuperável acontece (um estado corrompido de banco ou parâmetros inválidos em camadas internas), o sistema deve aplicar o princípio Fail Fast: falhar imediatamente na raiz, sem tentar mascarar o erro ou continuar executando operações com premissas inválidas.
Na camada de exposição HTTP, falhas de negócio devem ser serializadas conforme a RFC 7807 (Problem Details):
{
"type": "https://api.empresa.com/erros/saldo-insuficiente",
"title": "Saldo Insuficiente",
"status": 422,
"detail": "A conta possui R$ 50,00 disponíveis, mas a transferência requisitada foi de R$ 120,00.",
"instance": "/contas/123/transferencias/trans-987"
}
Separar com clareza o que é erro esperado de negócio (modelado via Result) do que é anomalia estrutural de ambiente (capturada pelo middleware global de exceções) estabiliza a arquitetura e simplifica os alarmes operacionais.