Capítulo 16: Tratamento Idiomático de Erros, Anyhow e Thiserror

Especialização em Sistemas com Rust • Rust 2021 Edition • Ownership, Concorrência Segura e Sistemas de Alta Performance


🗺️ Mapa Conceitual da Arquitetura Corporativa de Tratamento de Erros

flowchart TD
    OrigemErro["Origem do Erro (I/O, Parsing, Rede, Regra de Negócio)"] --> Classificacao["Decisão de Design"]

    subgraph CRATES_LIVRARIAS["Camada de Domínio / Bibliotecas"]
        ThiserrorCrate["thiserror: Enums Fortemente Tipados com #[derive(Error)]"]
        VariantesEspecificas["Variantes Específicas: ErroAuth, ErroDB, ErroValidacao"]
        ImplementaStd["Implementação Automática de std::error::Error + Display"]
    end

    subgraph APLICACOES_BINA["Camada de Aplicação / Binários / CLI"]
        AnyhowCrate["anyhow::Result<T>: Tipo Dinâmico de Alta Ergonomia"]
        Contexto[".context('Mensagem Explicativa'): Encadeamento de Causa"]
        Backtraces["Backtraces Completos em Falhas para Diagnóstico SRE"]
    end

    Classificacao -->|"Bibliotecas / Core"| CRATES_LIVRARIAS
    Classificacao -->|"Aplicações / Serviços"| APLICACOES_BINA

    CRATES_LIVRARIAS -->|"Propagação com '?'"| APLICACOES_BINA
    APLICACOES_BINA --> LogsRelatorios["Relatórios Limpos de Produção e Observabilidade"]

    style OrigemErro fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style Classificacao fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style CRATES_LIVRARIAS fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style APLICACOES_BINA fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style LogsRelatorios fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px

🏛️ 1. Filosofia de Resiliência e Tratamento de Erros

Em arquitetura de sistemas corporativos, um dos maiores desafios é garantir que falhas transientes (como timeout de rede, arquivo ausente ou dados corrompidos) sejam tratadas graciosamente, sem derrubar a aplicação.

O Rust adota uma filosofia transparente dividida em duas categorias:

  1. Erros Irrecuperáveis (panic!): Reservados para bugs graves de programação (como indexação fora dos limites de um array ou violação de invariantes críticas do sistema). O processo é abortado ou a stack é desenrolada.
  2. Erros Recuperáveis (Result<T, E>): O padrão para 99% das operações do mundo real. O erro faz parte explícita da assinatura de tipos da função, obrigando o chamador a tratá-lo antes de compilar.

⚙️ 2. A Trait Canônica std::error::Error

Para que um tipo seja reconhecido oficialmente como um erro pelo ecossistema do Rust, ele deve implementar três traits:

  • Debug: Para formatação técnica interna.
  • Display: Para mensagens amigáveis ao usuário final.
  • std::error::Error: Trait que define a inspeção de causas subjacentes com o método opcional .source().

📚 3. Tratamento de Erros em Bibliotecas com thiserror

Ao desenvolver bibliotecas, crates modulares ou a camada de domínio da aplicação, os erros devem ser fortemente tipados, permitindo que os consumidores façam pattern matching sobre as causas exatas do erro.

A crate thiserror elimina dezenas de linhas de boilerplate gerando as implementações de Display e Error automaticamente via macros procedurais:

// Cargo.toml: thiserror = "1.0"
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ErroCluster {
    #[error("Falha de I/O no disco do nó {no_id}: {source}")]
    FalhaDisco {
        no_id: u32,
        #[source]
        source: std::io::Error, // Encadeamento automático de causa
    },

    #[error("Timeout de conexão após {0} ms")]
    TimeoutRede(u64),

    #[error("Capacidade máxima do pool atingida ({limite} conexões)")]
    PoolExcedido { limite: u32 },

    #[error("Resposta do servidor não pôde ser parseada como JSON válido")]
    JsonInvalido(#[from] serde_json::Error), // Converte automaticamente com '?'!
}

🚀 4. Tratamento de Erros em Aplicações com anyhow

Enquanto bibliotecas precisam de enums rígidos, no nível de aplicação (como comandos CLI, controladores web ou tarefas em background), o que você mais precisa é de ergonomia, rapidez e adição de contexto sobre onde a falha ocorreu.

A crate anyhow fornece o tipo anyhow::Result<T> e métodos de enriquecimento com .context():

// Cargo.toml: anyhow = "1.0"
use anyhow::{Context, Result};
use std::fs::File;

fn inicializar_configuracao_empresa(caminho: &str) -> Result<Config> {
    // .with_context adiciona mensagens ricas sem perder a causa raiz original
    let arquivo = File::open(caminho)
        .with_context(|| format!("Não foi possível abrir o arquivo de configuração em '{}'", caminho))?;

    let config: Config = serde_json::from_reader(arquivo)
        .context("O conteúdo do arquivo JSON de configuração está corrompido ou incompatível")?;

    Ok(config)
}

Se o arquivo não existir, o anyhow exibirá no log a pilha explicativa completa:

Error: Não foi possível abrir o arquivo de configuração em '/etc/app/config.json'

Caused by:
    No such file or directory (os error 2)

⚖️ 5. A Regra de Ouro: thiserror vs anyhow

Critério thiserror anyhow
Onde Utilizar Bibliotecas, Crates, Camada de Domínio Aplicações Finais, Binários, CLIs, APIs
Tipo de Erro Enums fortemente tipados (ErroAuth, ErroDB) Objeto dinâmico (anyhow::Error / Result<T>)
Objetivo Principal Permitir ao chamador inspecionar e tratar cada caso com match Facilitar a propagação rápida com ? e enriquecer com .context()
Flexibilidade Rígida e previsível Extremamente ergonômica

🛠️ 6. Combinadores Funcionais em Result e Option

Rust permite encadear operações em resultados sem recorrer a múltiplos blocos condicionais:

// 1. .map() e .map_err(): Transforma o valor ou o erro
let numero: Result<i32, _> = "42".parse().map(|n: i32| n * 2);

// 2. .and_then(): Encadeia operações falíveis (Monadic FlatMap)
let resultado = buscar_usuario_id(10)
    .and_then(|u| carregar_carteira(u.carteira_id));

// 3. .unwrap_or_else(): Recuperação graciosa com fallback
let porta: u16 = std::env::var("PORTA")
    .ok() // Transforma Result em Option
    .and_then(|p| p.parse().ok())
    .unwrap_or(8080); // Porta padrão caso não esteja configurada

⚠️ 7. Armadilhas Comuns e Anti-Patterns

  1. Retornar Result<(), String>:
    • Anti-Pattern: Usar strings como tipo de erro (Err("Falhou")). Strings não implementam std::error::Error, perdem tipos e dificultam o tratamento programático.
    • Correção: Use enums com thiserror ou adote anyhow::Result.
  2. Ignorar Erros Silenciosamente com let _ = ...:
    • Descartar o Result sem inspecioná-lo oculta falhas críticas de infraestrutura. Trate com if let Err(e) = ... e registre em logs.

🛡️ 8. Boas Práticas de Observabilidade

  • Ative Backtraces em Ambientes de Teste: Configure a variável de ambiente RUST_BACKTRACE=1 para inspecionar o rastreamento completo da linha de código em caso de panics durante testes unitários.

📋 9. Cheat-Sheet: Resumo Executivo

// Biblioteca: thiserror
#[derive(thiserror::Error, Debug)]
pub enum MyError {
    #[error("Timeout: {0}s")]
    Timeout(u32),
}

// Aplicação: anyhow
use anyhow::{Context, Result};
fn run() -> Result<()> {
    operacao().context("Falha na etapa de inicialização")?;
    Ok(())
}