---
title: "Anyhow vs Thiserror: Error Handling Rust | Rust Brasil"
url: "https://rustlang.com.br/ecossistema/anyhow-thiserror/"
markdown_url: "https://rustlang.com.br/ecossistema/anyhow-thiserror.MD"
description: "Guia completo de anyhow e thiserror em Rust: erros personalizados, contexto, downcasting e boas práticas de error handling."
date: ""
author: ""
---

# 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`:

```toml
[dependencies]
thiserror = "2"
```

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

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

## Uso Básico

### Thiserror: Tipos de Erro Personalizados

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

```rust
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`:

```rust
// 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`:

```rust
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ário | Crate | Motivo |
|---------|-------|--------|
| Biblioteca pública | `thiserror` | Consumidores precisam fazer pattern matching nos erros |
| Aplicação (binário) | `anyhow` | Erros são reportados ao usuário, não inspecionados programaticamente |
| Biblioteca interna | `anyhow` ou `thiserror` | Depende se outros módulos inspecionam os erros |
| Protótipo rápido | `anyhow` | Mínimo boilerplate |

## Recursos Avançados

### Thiserror: Source e Backtrace

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

```rust
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:

```rust
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:

```rust
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!

```rust
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:

```rust
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:

```rust
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:

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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

| Crate | Uso | Vantagem |
|-------|-----|----------|
| `thiserror` | Bibliotecas | Derive macro, zero overhead |
| `anyhow` | Aplicações | Ergonomia, contexto rico |
| `eyre` | Aplicações | Similar ao anyhow, relatórios customizáveis |
| `color-eyre` | Aplicações | Relatórios coloridos com spans |
| `snafu` | Ambos | Mais verboso, mas mais explícito |
| `miette` | CLIs | Relató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](/ecossistema/log-env-logger/) para combinar logging com tratamento de erros
- Veja como frameworks web como [Axum](/ecossistema/axum/) integram com tipos de erro customizados
- Aprenda sobre [tracing](/ecossistema/tracing/) para capturar contexto de erro em spans assíncronos
