Anyhow vs Thiserror: Error Handling Rust | Rust Brasil

Guia completo de anyhow e thiserror em Rust: erros personalizados, contexto, downcasting e boas práticas de error handling.

O tratamento de erros é uma das áreas onde Rust mais se destaca. O sistema de tipos com Result<T, E> e o operador ? já fornecem uma base sólida, mas as crates thiserror e anyhow elevam essa experiência a outro patamar. Juntas, elas formam o “duo dinâmico” do tratamento de erros no ecossistema Rust.

thiserror é projetada para bibliotecas — permite definir tipos de erro personalizados e expressivos usando derive macros, implementando automaticamente std::error::Error e Display. anyhow é projetada para aplicações — oferece um tipo de erro flexível e ergonômico que aceita qualquer erro e permite adicionar contexto rico.

Instalação

Para bibliotecas, adicione thiserror:

[dependencies]
thiserror = "2"

Para aplicações, adicione anyhow (e opcionalmente thiserror para erros internos):

[dependencies]
anyhow = "1"
thiserror = "2"

Uso Básico

Thiserror: Tipos de Erro Personalizados

O thiserror simplifica drasticamente a criação de tipos de erro:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("Falha ao ler arquivo de configuração: {0}")]
    ConfigInvalida(String),

    #[error("Usuário '{nome}' não encontrado (id: {id})")]
    UsuarioNaoEncontrado { id: u64, nome: String },

    #[error("Erro de I/O")]
    Io(#[from] std::io::Error),

    #[error("Erro de parsing JSON")]
    Json(#[from] serde_json::Error),

    #[error("Timeout após {0} segundos")]
    Timeout(u64),
}

Vamos entender cada atributo:

  • #[error("...")] — define a mensagem de Display
  • {0}, {nome} — interpolação de campos na mensagem
  • #[from] — implementa From<T> automaticamente, permitindo usar ?

Sem thiserror, você precisaria implementar manualmente Display, Error e From:

// Sem thiserror — muito verboso!
use std::fmt;

#[derive(Debug)]
pub enum AppError {
    ConfigInvalida(String),
    Io(std::io::Error),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::ConfigInvalida(msg) => write!(f, "Config inválida: {}", msg),
            AppError::Io(e) => write!(f, "Erro de I/O: {}", e),
        }
    }
}

impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::Io(e)
    }
}

Anyhow: Erros Flexíveis para Aplicações

O anyhow fornece o tipo anyhow::Result<T> e o tipo anyhow::Error:

use anyhow::{Context, Result};
use std::fs;

fn ler_configuracao(caminho: &str) -> Result<String> {
    let conteudo = fs::read_to_string(caminho)
        .context("Falha ao ler arquivo de configuração")?;

    if conteudo.is_empty() {
        anyhow::bail!("Arquivo de configuração está vazio");
    }

    Ok(conteudo)
}

fn main() -> Result<()> {
    let config = ler_configuracao("config.toml")?;
    println!("Config: {}", config);
    Ok(())
}

Principais recursos do anyhow:

  • Result<T> — alias para Result<T, anyhow::Error>
  • .context("mensagem") — adiciona contexto humano ao erro
  • bail!("msg") — retorna erro imediatamente (como return Err(...))
  • ensure!(condição, "msg") — como assert!, mas retorna Result
  • anyhow!("msg") — cria um anyhow::Error ad hoc

Quando Usar Qual

A regra é simples:

CenárioCrateMotivo
Biblioteca públicathiserrorConsumidores precisam fazer pattern matching nos erros
Aplicação (binário)anyhowErros são reportados ao usuário, não inspecionados programaticamente
Biblioteca internaanyhow ou thiserrorDepende se outros módulos inspecionam os erros
Protótipo rápidoanyhowMínimo boilerplate

Recursos Avançados

Thiserror: Source e Backtrace

O #[source] permite encadear erros sem implementar From:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DatabaseError {
    #[error("Falha na conexão com o banco")]
    Conexao {
        #[source]
        causa: std::io::Error,
        tentativas: u32,
    },

    #[error("Query inválida: {query}")]
    QueryInvalida {
        query: String,
        #[source]
        causa: sqlx::Error,
    },

    #[error("Pool de conexões esgotado (max: {max})")]
    PoolEsgotado { max: usize },

    #[error(transparent)]
    Outro(#[from] anyhow::Error),
}

O #[error(transparent)] delega tanto Display quanto source() para o erro interno — útil para “catch-all”.

Thiserror: Erros Genéricos

Você pode criar tipos de erro genéricos:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ParseError<T: std::fmt::Debug> {
    #[error("Valor inválido: {0:?}")]
    ValorInvalido(T),

    #[error("Entrada vazia")]
    EntradaVazia,

    #[error("Formato inesperado na posição {posicao}")]
    FormatoInvalido { posicao: usize },
}

Anyhow: Contexto Encadeado

O .context() pode ser encadeado para criar uma trilha de contexto:

use anyhow::{Context, Result};
use std::fs;

#[derive(serde::Deserialize)]
struct Config {
    porta: u16,
    host: String,
    banco_url: String,
}

fn carregar_config(caminho: &str) -> Result<Config> {
    let conteudo = fs::read_to_string(caminho)
        .with_context(|| format!("Falha ao ler '{}'", caminho))?;

    let config: Config = toml::from_str(&conteudo)
        .with_context(|| format!("Falha ao parsear TOML em '{}'", caminho))?;

    if config.porta == 0 {
        anyhow::bail!("Porta não pode ser 0 em '{}'", caminho);
    }

    Ok(config)
}

fn iniciar_servidor() -> Result<()> {
    let config = carregar_config("config.toml")
        .context("Falha ao inicializar servidor")?;

    println!("Servidor em {}:{}", config.host, config.porta);
    Ok(())
}

Quando um erro ocorre, a saída mostra toda a cadeia:

Error: Falha ao inicializar servidor

Caused by:
    0: Falha ao ler 'config.toml'
    1: No such file or directory (os error 2)

Anyhow: bail! e ensure!

use anyhow::{bail, ensure, Result};

fn validar_idade(idade: i32) -> Result<()> {
    ensure!(idade >= 0, "Idade não pode ser negativa: {}", idade);
    ensure!(idade <= 150, "Idade inválida: {} (máximo: 150)", idade);

    if idade < 18 {
        bail!("Menor de idade: {} anos. Acesso negado.", idade);
    }

    Ok(())
}

fn processar_entrada(entrada: &str) -> Result<i32> {
    if entrada.is_empty() {
        bail!("Entrada vazia não é permitida");
    }

    let valor: i32 = entrada.parse()
        .with_context(|| format!("'{}' não é um número válido", entrada))?;

    validar_idade(valor)?;

    Ok(valor)
}

Downcasting: Recuperando o Tipo Original

O anyhow::Error permite recuperar o tipo de erro original via downcasting:

use anyhow::{Context, Result};
use thiserror::Error;

#[derive(Error, Debug)]
enum MeuErro {
    #[error("Recurso não encontrado: {0}")]
    NaoEncontrado(String),

    #[error("Sem permissão para: {0}")]
    SemPermissao(String),

    #[error("Limite excedido: {atual}/{maximo}")]
    LimiteExcedido { atual: u64, maximo: u64 },
}

fn buscar_recurso(id: &str) -> Result<String> {
    if id == "secreto" {
        Err(MeuErro::SemPermissao(id.to_string()))?;
    }
    if id == "inexistente" {
        Err(MeuErro::NaoEncontrado(id.to_string()))?;
    }
    Ok(format!("Dados do recurso {}", id))
}

fn tratar_resultado(id: &str) {
    match buscar_recurso(id) {
        Ok(dados) => println!("Sucesso: {}", dados),
        Err(e) => {
            // Downcasting para o tipo original
            if let Some(MeuErro::NaoEncontrado(recurso)) = e.downcast_ref::<MeuErro>() {
                println!("Recurso '{}' não existe. Criar novo?", recurso);
            } else if let Some(MeuErro::SemPermissao(recurso)) = e.downcast_ref::<MeuErro>() {
                println!("Acesso negado ao recurso '{}'. Solicitar permissão.", recurso);
            } else {
                println!("Erro inesperado: {:#}", e);
            }
        }
    }
}

Combinando Thiserror e Anyhow

O padrão mais comum é usar thiserror para erros de domínio e anyhow na camada de aplicação:

use anyhow::{Context, Result};
use thiserror::Error;

// --- Camada de domínio (thiserror) ---

#[derive(Error, Debug)]
pub enum AuthError {
    #[error("Credenciais inválidas para o usuário '{usuario}'")]
    CredenciaisInvalidas { usuario: String },

    #[error("Token expirado")]
    TokenExpirado,

    #[error("Conta bloqueada após {tentativas} tentativas")]
    ContaBloqueada { tentativas: u32 },
}

#[derive(Error, Debug)]
pub enum PedidoError {
    #[error("Produto '{0}' sem estoque")]
    SemEstoque(String),

    #[error("Valor do pedido excede limite: R${valor:.2} > R${limite:.2}")]
    LimiteExcedido { valor: f64, limite: f64 },

    #[error("Pedido #{0} não encontrado")]
    NaoEncontrado(u64),
}

// --- Camada de serviço (thiserror para erros que a app pode inspecionar) ---

#[derive(Error, Debug)]
pub enum ServicoError {
    #[error("Erro de autenticação")]
    Auth(#[from] AuthError),

    #[error("Erro no pedido")]
    Pedido(#[from] PedidoError),

    #[error("Erro interno: {0}")]
    Interno(#[from] anyhow::Error),
}

// --- Camada de aplicação (anyhow) ---

fn autenticar(usuario: &str, senha: &str) -> std::result::Result<String, AuthError> {
    if usuario == "admin" && senha == "1234" {
        Ok("token_abc123".to_string())
    } else {
        Err(AuthError::CredenciaisInvalidas {
            usuario: usuario.to_string(),
        })
    }
}

fn criar_pedido(produto: &str, quantidade: u32) -> std::result::Result<u64, PedidoError> {
    if produto == "esgotado" {
        return Err(PedidoError::SemEstoque(produto.to_string()));
    }
    let valor = quantidade as f64 * 99.90;
    if valor > 10_000.0 {
        return Err(PedidoError::LimiteExcedido {
            valor,
            limite: 10_000.0,
        });
    }
    Ok(42) // ID do pedido
}

fn processar_compra(usuario: &str, senha: &str, produto: &str) -> Result<String> {
    let token = autenticar(usuario, senha)
        .context("Falha na autenticação do usuário")?;

    let pedido_id = criar_pedido(produto, 1)
        .context("Falha ao criar pedido")?;

    Ok(format!("Pedido #{} criado com token {}", pedido_id, token))
}

fn main() -> Result<()> {
    match processar_compra("admin", "1234", "notebook") {
        Ok(msg) => println!("Sucesso: {}", msg),
        Err(e) => {
            eprintln!("Erro: {:#}", e);

            // Podemos inspecionar a cadeia de erros
            for causa in e.chain() {
                eprintln!("  Causado por: {}", causa);
            }
        }
    }

    Ok(())
}

Formatação de Erros

O anyhow suporta diferentes formatos de exibição:

use anyhow::{Context, Result};

fn exemplo() -> Result<()> {
    std::fs::read_to_string("/inexistente")
        .context("Lendo configuração")
        .context("Inicializando aplicação")?;
    Ok(())
}

fn main() {
    if let Err(e) = exemplo() {
        // Display simples (apenas a mensagem principal)
        println!("Display: {}", e);
        // Saída: Inicializando aplicação

        // Display alternativo (cadeia completa)
        println!("Debug alt: {:#}", e);
        // Saída: Inicializando aplicação: Lendo configuração: No such file...

        // Debug (com backtrace se disponível)
        println!("Debug: {:?}", e);

        // Iterando pela cadeia de erros
        println!("\nCadeia de erros:");
        for (i, causa) in e.chain().enumerate() {
            println!("  {}: {}", i, causa);
        }
    }
}

Boas Práticas

1. Erros de Biblioteca Devem Ser Enums

use thiserror::Error;

// BOM: enum permite pattern matching pelo consumidor
#[derive(Error, Debug)]
pub enum ParseError {
    #[error("Token inesperado '{token}' na posição {posicao}")]
    TokenInesperado { token: String, posicao: usize },

    #[error("Fim inesperado da entrada")]
    FimInesperado,

    #[error("Profundidade máxima excedida: {0}")]
    ProfundidadeExcedida(usize),
}

// RUIM: String genérica não permite inspeção programática
// pub fn parsear(input: &str) -> Result<Ast, String> { ... }

2. Use #[from] com Parcimônia

use thiserror::Error;

#[derive(Error, Debug)]
pub enum StorageError {
    // BOM: conversão automática faz sentido aqui
    #[error("Erro de I/O")]
    Io(#[from] std::io::Error),

    // CUIDADO: se houver múltiplos erros de I/O com significados diferentes,
    // use #[source] em vez de #[from]
    #[error("Falha ao ler dados")]
    LeituraFalhou {
        caminho: String,
        #[source]
        causa: std::io::Error,
    },

    #[error("Falha ao escrever dados")]
    EscritaFalhou {
        caminho: String,
        #[source]
        causa: std::io::Error,
    },
}

3. Contexto Rico com with_context

use anyhow::{Context, Result};
use std::path::Path;

fn processar_arquivo(caminho: &Path) -> Result<Vec<u8>> {
    // RUIM: contexto genérico
    // let dados = std::fs::read(caminho).context("Falha ao ler arquivo")?;

    // BOM: contexto inclui informações úteis
    let dados = std::fs::read(caminho)
        .with_context(|| format!(
            "Falha ao ler arquivo '{}' ({} bytes esperados)",
            caminho.display(),
            caminho.metadata().map(|m| m.len()).unwrap_or(0)
        ))?;

    Ok(dados)
}

4. Não Descarte a Cadeia de Erros

use anyhow::Result;

// RUIM: perde a causa original
fn ruim(caminho: &str) -> Result<String> {
    match std::fs::read_to_string(caminho) {
        Ok(s) => Ok(s),
        Err(_) => anyhow::bail!("Falha ao ler arquivo"),
    }
}

// BOM: preserva a causa e adiciona contexto
fn bom(caminho: &str) -> Result<String> {
    std::fs::read_to_string(caminho)
        .with_context(|| format!("Falha ao ler '{}'", caminho))
}

5. Trate Erros na Fronteira

use anyhow::Result;

// Funções internas propagam erros
fn carregar_dados() -> Result<Vec<String>> {
    let conteudo = std::fs::read_to_string("dados.txt")
        .context("Falha ao carregar dados")?;
    Ok(conteudo.lines().map(String::from).collect())
}

// A fronteira (main, handlers HTTP, etc.) trata os erros
fn main() {
    match carregar_dados() {
        Ok(dados) => {
            println!("Carregados {} registros", dados.len());
            for d in &dados {
                println!("  - {}", d);
            }
        }
        Err(e) => {
            // Exibe a cadeia completa para o usuário
            eprintln!("Erro: {:#}", e);
            std::process::exit(1);
        }
    }
}

Exemplos Práticos

Exemplo Completo: Aplicação de Processamento de Pedidos

use anyhow::{bail, ensure, Context, Result};
use thiserror::Error;
use std::collections::HashMap;

// === Erros de Domínio (thiserror) ===

#[derive(Error, Debug)]
pub enum ValidacaoError {
    #[error("Campo obrigatório ausente: {0}")]
    CampoAusente(String),

    #[error("Email inválido: '{0}'")]
    EmailInvalido(String),

    #[error("CPF inválido: '{0}'")]
    CpfInvalido(String),

    #[error("Valor fora do intervalo: {valor} (esperado: {min}..{max})")]
    ForaDoIntervalo { valor: f64, min: f64, max: f64 },
}

#[derive(Error, Debug)]
pub enum EstoqueError {
    #[error("Produto '{produto}' sem estoque (disponível: {disponivel}, solicitado: {solicitado})")]
    Insuficiente {
        produto: String,
        disponivel: u32,
        solicitado: u32,
    },

    #[error("Produto não cadastrado: '{0}'")]
    ProdutoNaoEncontrado(String),
}

#[derive(Error, Debug)]
pub enum PagamentoError {
    #[error("Cartão recusado: {motivo}")]
    CartaoRecusado { motivo: String },

    #[error("Saldo insuficiente: R${saldo:.2} < R${valor:.2}")]
    SaldoInsuficiente { saldo: f64, valor: f64 },
}

// === Modelos ===

#[derive(Debug, Clone)]
struct Cliente {
    nome: String,
    email: String,
    cpf: String,
}

#[derive(Debug, Clone)]
struct ItemPedido {
    produto: String,
    quantidade: u32,
    preco_unitario: f64,
}

#[derive(Debug)]
struct Pedido {
    id: u64,
    cliente: Cliente,
    itens: Vec<ItemPedido>,
}

impl Pedido {
    fn valor_total(&self) -> f64 {
        self.itens.iter()
            .map(|i| i.preco_unitario * i.quantidade as f64)
            .sum()
    }
}

// === Serviços ===

fn validar_email(email: &str) -> std::result::Result<(), ValidacaoError> {
    if !email.contains('@') || !email.contains('.') {
        return Err(ValidacaoError::EmailInvalido(email.to_string()));
    }
    Ok(())
}

fn validar_cpf(cpf: &str) -> std::result::Result<(), ValidacaoError> {
    let digitos: String = cpf.chars().filter(|c| c.is_ascii_digit()).collect();
    if digitos.len() != 11 {
        return Err(ValidacaoError::CpfInvalido(cpf.to_string()));
    }
    Ok(())
}

fn validar_cliente(cliente: &Cliente) -> std::result::Result<(), ValidacaoError> {
    if cliente.nome.is_empty() {
        return Err(ValidacaoError::CampoAusente("nome".to_string()));
    }
    validar_email(&cliente.email)?;
    validar_cpf(&cliente.cpf)?;
    Ok(())
}

fn verificar_estoque(
    estoque: &HashMap<String, u32>,
    itens: &[ItemPedido],
) -> std::result::Result<(), EstoqueError> {
    for item in itens {
        match estoque.get(&item.produto) {
            None => return Err(EstoqueError::ProdutoNaoEncontrado(item.produto.clone())),
            Some(&disponivel) if disponivel < item.quantidade => {
                return Err(EstoqueError::Insuficiente {
                    produto: item.produto.clone(),
                    disponivel,
                    solicitado: item.quantidade,
                });
            }
            _ => {}
        }
    }
    Ok(())
}

fn processar_pagamento(
    valor: f64,
    saldo: f64,
) -> std::result::Result<String, PagamentoError> {
    if valor > 50_000.0 {
        return Err(PagamentoError::CartaoRecusado {
            motivo: "Valor excede limite diário".to_string(),
        });
    }
    if saldo < valor {
        return Err(PagamentoError::SaldoInsuficiente { saldo, valor });
    }
    Ok(format!("PAG-{}", rand_id()))
}

fn rand_id() -> u64 {
    // Simplificação - em produção usaria uuid ou similar
    42_12345
}

// === Orquestração com Anyhow ===

fn criar_pedido(
    cliente: Cliente,
    itens: Vec<ItemPedido>,
    estoque: &HashMap<String, u32>,
    saldo_cliente: f64,
) -> Result<Pedido> {
    // Validar cliente
    validar_cliente(&cliente)
        .context("Validação do cliente falhou")?;

    // Validar itens
    ensure!(!itens.is_empty(), "Pedido deve ter pelo menos um item");

    for item in &itens {
        ensure!(
            item.quantidade > 0,
            "Quantidade inválida para '{}': {}",
            item.produto,
            item.quantidade
        );
        ensure!(
            item.preco_unitario > 0.0,
            "Preço inválido para '{}': R${:.2}",
            item.produto,
            item.preco_unitario
        );
    }

    // Verificar estoque
    verificar_estoque(estoque, &itens)
        .context("Verificação de estoque falhou")?;

    // Criar pedido
    let pedido = Pedido {
        id: rand_id(),
        cliente,
        itens,
    };

    // Processar pagamento
    let comprovante = processar_pagamento(pedido.valor_total(), saldo_cliente)
        .with_context(|| {
            format!(
                "Pagamento de R${:.2} para pedido #{} falhou",
                pedido.valor_total(),
                pedido.id
            )
        })?;

    println!("Pagamento aprovado: {}", comprovante);
    Ok(pedido)
}

fn main() -> Result<()> {
    // Configurar estoque
    let mut estoque = HashMap::new();
    estoque.insert("notebook".to_string(), 10);
    estoque.insert("mouse".to_string(), 50);
    estoque.insert("teclado".to_string(), 30);

    // Cenário 1: Pedido válido
    let cliente = Cliente {
        nome: "Maria Silva".to_string(),
        email: "maria@exemplo.com".to_string(),
        cpf: "123.456.789-00".to_string(),
    };

    let itens = vec![
        ItemPedido {
            produto: "notebook".to_string(),
            quantidade: 1,
            preco_unitario: 4500.0,
        },
        ItemPedido {
            produto: "mouse".to_string(),
            quantidade: 2,
            preco_unitario: 89.90,
        },
    ];

    match criar_pedido(cliente, itens, &estoque, 5000.0) {
        Ok(pedido) => {
            println!("Pedido #{} criado com sucesso!", pedido.id);
            println!("  Cliente: {}", pedido.cliente.nome);
            println!("  Valor total: R${:.2}", pedido.valor_total());
        }
        Err(e) => {
            eprintln!("Falha ao criar pedido:");
            for causa in e.chain() {
                eprintln!("  -> {}", causa);
            }
        }
    }

    // Cenário 2: Saldo insuficiente
    let cliente2 = Cliente {
        nome: "João Santos".to_string(),
        email: "joao@exemplo.com".to_string(),
        cpf: "987.654.321-00".to_string(),
    };

    let itens2 = vec![ItemPedido {
        produto: "notebook".to_string(),
        quantidade: 3,
        preco_unitario: 4500.0,
    }];

    match criar_pedido(cliente2, itens2, &estoque, 1000.0) {
        Ok(pedido) => println!("Pedido #{} criado", pedido.id),
        Err(e) => {
            eprintln!("\nFalha esperada:");
            eprintln!("{:#}", e);

            // Podemos inspecionar o tipo de erro
            if let Some(pe) = e.downcast_ref::<PagamentoError>() {
                match pe {
                    PagamentoError::SaldoInsuficiente { saldo, valor } => {
                        eprintln!(
                            "  Sugestão: deposite R${:.2} para completar a compra",
                            valor - saldo
                        );
                    }
                    PagamentoError::CartaoRecusado { motivo } => {
                        eprintln!("  Tente outro cartão: {}", motivo);
                    }
                }
            }
        }
    }

    Ok(())
}

Exemplo: Trait de Erro para APIs HTTP

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ApiError {
    #[error("Recurso não encontrado")]
    NaoEncontrado,

    #[error("Não autorizado")]
    NaoAutorizado,

    #[error("Dados inválidos: {0}")]
    DadosInvalidos(String),

    #[error("Conflito: {0}")]
    Conflito(String),

    #[error("Erro interno")]
    Interno(#[from] anyhow::Error),
}

impl ApiError {
    pub fn status_code(&self) -> u16 {
        match self {
            ApiError::NaoEncontrado => 404,
            ApiError::NaoAutorizado => 401,
            ApiError::DadosInvalidos(_) => 400,
            ApiError::Conflito(_) => 409,
            ApiError::Interno(_) => 500,
        }
    }

    pub fn corpo_json(&self) -> String {
        format!(
            r#"{{"erro": "{}", "codigo": {}}}"#,
            self,
            self.status_code()
        )
    }
}

// Em um handler Axum, por exemplo:
// impl IntoResponse for ApiError {
//     fn into_response(self) -> Response {
//         (StatusCode::from_u16(self.status_code()).unwrap(),
//          Json(self.corpo_json())).into_response()
//     }
// }

Comparação com Alternativas

CrateUsoVantagem
thiserrorBibliotecasDerive macro, zero overhead
anyhowAplicaçõesErgonomia, contexto rico
eyreAplicaçõesSimilar ao anyhow, relatórios customizáveis
color-eyreAplicaçõesRelatórios coloridos com spans
snafuAmbosMais verboso, mas mais explícito
mietteCLIsRelatórios de erro bonitos com source code

Conclusão

thiserror e anyhow resolvem lados complementares do tratamento de erros em Rust. Use thiserror em bibliotecas para criar tipos de erro expressivos e inspecionáveis. Use anyhow em aplicações para tratamento ergonômico com contexto rico.

A combinação das duas crates, junto com o operador ? e o sistema de tipos de Rust, resulta em código que é tanto robusto quanto legível — erros nunca são silenciosamente ignorados, a cadeia de causas é preservada, e o programador tem controle total sobre como cada tipo de erro é tratado.

Próximos passos:

  • Explore log e env_logger para combinar logging com tratamento de erros
  • Veja como frameworks web como Axum integram com tipos de erro customizados
  • Aprenda sobre tracing para capturar contexto de erro em spans assíncronos