Software · Design Patterns

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.

Diagrama conceitual ilustrando tratamento de erros: exceções, result types e fail fast
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.

Explorar este tema no Grafo de Conhecimento