Thiserror e Anyhow em Rust: Qual Usar? | Rust Brasil

Thiserror ou anyhow em Rust? Compare erros tipados, contexto, bibliotecas e aplicações. Veja quando usar cada crate, exemplos e boas práticas.

Resposta curta: use thiserror quando o chamador precisa identificar variantes do erro — principalmente em bibliotecas, domínio e APIs. Use anyhow quando uma aplicação ou CLI precisa propagar falhas e acrescentar contexto sem expor um enum público. Em projetos maiores, use os dois: thiserror nas camadas internas e anyhow nas bordas.

Thiserror vs anyhow: decisão rápida

SituaçãoEscolhaMotivo
Crate ou biblioteca públicathiserrorO consumidor pode fazer match nas variantes
Domínio, repositório ou serviçothiserrorMantém contratos de erro explícitos e testáveis
Binário, CLI, job ou scriptanyhowSimplifica propagação, relatório e contexto
main, bootstrap e tarefas administrativasanyhowA borda normalmente precisa exibir a causa, não classificá-la
API HTTPAmbosthiserror modela falhas; a borda pode agregar contexto e logs
Protótipo pequenoanyhowMenos boilerplate para começar

A regra prática para thiserror vs anyhow é perguntar: quem recebe o erro precisa reagir de forma diferente conforme a variante? Se sim, prefira thiserror. Se o erro só precisa subir até uma borda, ganhar contexto e ser registrado ou exibido, prefira anyhow.

O tratamento de erros é uma das áreas onde Rust mais se diferencia de outras linguagens. Enquanto linguagens como Java e Python usam exceções que podem surgir em muitos pontos do código, Rust torna os erros parte do sistema de tipos com Result<T, E>: se uma função pode falhar, sua assinatura comunica essa possibilidade.

Trabalhar apenas com Result e implementações manuais de Display, Error e From pode ser verboso. As crates thiserror e anyhow resolvem problemas complementares: a primeira reduz o boilerplate de erros tipados; a segunda facilita reunir causas diferentes e acrescentar contexto humano. Este guia mostra como escolher, combinar e aplicar as duas em produção.

Se você ainda está aprendendo os conceitos básicos, comece pelo tutorial de tratamento de erros e pela referência de Result. Para uma página de consulta sobre as crates, veja também anyhow e thiserror no ecossistema Rust.

Result e Option — A Base de Tudo

Em Rust, o tipo Result<T, E> representa uma operação que pode ter sucesso (Ok(T)) ou falhar (Err(E)). Já o Option<T> representa um valor que pode existir (Some(T)) ou não (None). Para uma referência completa desses tipos, veja nossos guias sobre Result e Option.

use std::fs;
use std::num::ParseIntError;

fn ler_numero_do_arquivo(caminho: &str) -> Result<i32, String> {
    // Lê o conteúdo do arquivo
    let conteudo = fs::read_to_string(caminho)
        .map_err(|e| format!("Erro ao ler arquivo: {}", e))?;

    // Converte para número
    let numero: i32 = conteudo.trim().parse()
        .map_err(|e: ParseIntError| format!("Erro ao converter: {}", e))?;

    Ok(numero)
}

O operador ? propaga o erro automaticamente, mas note como precisamos usar map_err para converter entre tipos de erro diferentes. Isso funciona, mas fica verboso rapidamente.

Erros Customizados com enum

A abordagem idiomática em Rust é criar um enum que represente todos os erros possíveis do seu módulo:

use std::fmt;
use std::io;
use std::num::ParseIntError;

#[derive(Debug)]
enum MeuErro {
    Io(io::Error),
    Parse(ParseIntError),
    Validacao(String),
}

impl fmt::Display for MeuErro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MeuErro::Io(e) => write!(f, "Erro de I/O: {}", e),
            MeuErro::Parse(e) => write!(f, "Erro de parsing: {}", e),
            MeuErro::Validacao(msg) => write!(f, "Validação: {}", msg),
        }
    }
}

impl std::error::Error for MeuErro {}

// Conversões automáticas com From
impl From<io::Error> for MeuErro {
    fn from(e: io::Error) -> Self {
        MeuErro::Io(e)
    }
}

impl From<ParseIntError> for MeuErro {
    fn from(e: ParseIntError) -> Self {
        MeuErro::Parse(e)
    }
}

Isso é correto e seguro, mas são mais de 30 linhas de boilerplate para três variantes de erro. Imagine um projeto real com dezenas de tipos de erro — é aqui que o thiserror brilha.

thiserror — Erros Tipados sem Boilerplate

O thiserror é uma crate de derive macro que gera automaticamente as implementações de Display, Error e From. É a escolha padrão para bibliotecas e código que precisa de erros tipados. Para entender como derive macros funcionam por baixo dos panos, confira nosso artigo sobre macros em Rust.

use thiserror::Error;

#[derive(Error, Debug)]
enum AppErro {
    #[error("Erro ao acessar arquivo: {0}")]
    Io(#[from] std::io::Error),

    #[error("Formato inválido: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("Validação falhou: {mensagem}")]
    Validacao { mensagem: String },

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

    #[error("Conexão com o banco falhou após {tentativas} tentativas")]
    ConexaoBanco { tentativas: u32 },
}

Com thiserror, o enum acima gera automaticamente:

  • impl Display usando as strings do atributo #[error(...)]
  • impl Error com a trait std::error::Error
  • impl From<std::io::Error> e impl From<ParseIntError> graças ao #[from]

Agora a função anterior fica muito mais limpa:

fn ler_numero_do_arquivo(caminho: &str) -> Result<i32, AppErro> {
    let conteudo = std::fs::read_to_string(caminho)?; // ? converte io::Error automaticamente
    let numero: i32 = conteudo.trim().parse()?;        // ? converte ParseIntError automaticamente

    if numero < 0 {
        return Err(AppErro::Validacao {
            mensagem: "Número deve ser positivo".into(),
        });
    }

    Ok(numero)
}

Padrões Avançados com thiserror

O thiserror suporta #[source] para encadear erros e #[backtrace] para capturar stack traces:

#[derive(Error, Debug)]
enum ServicoErro {
    #[error("Falha na requisição HTTP")]
    Http {
        #[source]
        causa: reqwest::Error,
        url: String,
    },

    #[error("Configuração inválida: {0}")]
    Config(String),

    #[error(transparent)] // Delega Display e source para o erro interno
    Outro(#[from] anyhow::Error),
}

anyhow — Erros Flexíveis para Aplicações

Enquanto o thiserror é ideal para bibliotecas com erros tipados, o anyhow é projetado para aplicações onde você quer simplicidade máxima. Ele fornece o tipo anyhow::Result<T> que aceita qualquer erro implementando std::error::Error. Para uma análise completa dessas duas crates, veja nosso guia do ecossistema anyhow e thiserror.

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

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

    let config: Config = toml::from_str(&conteudo)
        .context("TOML inválido no arquivo de configuração")?;

    // bail! retorna Err imediatamente com uma mensagem
    if config.porta == 0 {
        bail!("Porta não pode ser zero");
    }

    // ensure! é como assert! mas retorna Err em vez de panic
    ensure!(config.porta < 65536, "Porta {} fora do intervalo válido", config.porta);

    Ok(config)
}

O método .context() é um dos recursos mais poderosos do anyhow — ele adiciona contexto humano aos erros, criando uma cadeia que facilita o diagnóstico:

Error: Falha ao processar pedido #42

Caused by:
    0: Falha ao ler arquivo de configuração
    1: No such file or directory (os error 2)

Quando usar anyhow vs thiserror

CenárioRecomendação
Biblioteca pública (crate)thiserror — consumidores precisam inspecionar variantes
Aplicação bináriaanyhow — contexto e relatório são mais importantes
Regra de domíniothiserror — o erro faz parte do contrato
API RESTthiserror para classificar falhas e convertê-las em status HTTP
Scripts, jobs e CLIsanyhow — propagar e explicar a falha costuma bastar

Na prática, muitos projetos usam ambos: thiserror define erros nas camadas de domínio, repositório e serviço; anyhow adiciona contexto no main, em comandos administrativos e em outras bordas. Evite retornar anyhow::Error em uma API pública quando os consumidores precisam distinguir “não encontrado”, “conflito” e “entrada inválida”.

O Operador ? em Profundidade

O operador ? é syntactic sugar que faz muito mais do que parece. Ele chama From::from() no erro para converter entre tipos, permitindo composição elegante. Para entender como o trait From funciona, veja nosso guia sobre From e Into.

use anyhow::{Context, Result};

// Composição de múltiplas operações falíveis
async fn buscar_usuario(id: u64) -> Result<Usuario> {
    let url = format!("https://api.exemplo.com/usuarios/{}", id);

    let resposta = reqwest::get(&url)
        .await
        .context("Falha na requisição HTTP")?;

    let usuario: Usuario = resposta
        .json()
        .await
        .context("Resposta não é JSON válido")?;

    // Validação com ensure!
    anyhow::ensure!(!usuario.nome.is_empty(), "Nome do usuário está vazio");

    Ok(usuario)
}

Se você está trabalhando com código assíncrono, o operador ? funciona perfeitamente com async/await — veja nosso guia completo sobre async Rust.

Padrão: Erros por Camada

Em aplicações maiores, um padrão comum é definir tipos de erro por camada usando thiserror e converter para anyhow nas bordas:

// Camada de repositório
#[derive(Error, Debug)]
enum RepoErro {
    #[error("Registro não encontrado: {0}")]
    NaoEncontrado(String),
    #[error("Violação de constraint: {0}")]
    Constraint(String),
    #[error("Erro de banco de dados")]
    Database(#[from] sqlx::Error),
}

// Camada de serviço
#[derive(Error, Debug)]
enum ServicoErro {
    #[error("Usuário não encontrado")]
    UsuarioNaoEncontrado,
    #[error("Email já cadastrado: {0}")]
    EmailDuplicado(String),
    #[error(transparent)]
    Interno(#[from] RepoErro),
}

// Camada de API — converte para respostas HTTP
impl axum::response::IntoResponse for ServicoErro {
    fn into_response(self) -> axum::response::Response {
        let (status, mensagem) = match &self {
            ServicoErro::UsuarioNaoEncontrado => {
                (StatusCode::NOT_FOUND, self.to_string())
            }
            ServicoErro::EmailDuplicado(_) => {
                (StatusCode::CONFLICT, self.to_string())
            }
            ServicoErro::Interno(_) => {
                (StatusCode::INTERNAL_SERVER_ERROR, "Erro interno".into())
            }
        };
        (status, Json(json!({ "erro": mensagem }))).into_response()
    }
}

Para aprender mais sobre como integrar esse padrão com Axum, confira nosso artigo Axum Web Framework.

Boas Práticas

  1. Nunca use .unwrap() em produção — use .expect("mensagem descritiva") ou trate o erro. Veja nosso guia sobre erros com panic e unwrap.

  2. Prefira ? sobre match quando só precisa propagar o erro.

  3. Adicione contexto com .context() ou .with_context(|| ...) para erros que cruzam fronteiras de módulo.

  4. Defina erros tipados para interfaces públicas — consumidores da sua API precisam fazer pattern matching.

  5. Use #[error(transparent)] quando uma variante de erro é apenas um wrapper.

  6. Teste seus erros — verifique que as mensagens são úteis e que os tipos corretos são retornados. Veja nosso tutorial de testes em Rust.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn teste_erro_validacao() {
        let resultado = validar_email("");
        assert!(resultado.is_err());
        let erro = resultado.unwrap_err();
        assert!(matches!(erro, AppErro::Validacao { .. }));
        assert!(erro.to_string().contains("Email"));
    }
}

Comparação com Outras Linguagens

O modelo de erros do Rust é frequentemente comparado ao de Go e ao de linguagens com exceções. Diferente de Go, onde o padrão if err != nil não é verificado pelo compilador, Rust garante em tempo de compilação que todos os erros sejam tratados. E diferente de Python, onde exceções podem surgir de qualquer lugar sem aviso, em Rust o tipo Result torna os caminhos de erro explícitos na assinatura da função.

Para comparações detalhadas entre Rust e outras linguagens, confira nossos artigos Rust vs Go e Rust vs Python.

Perguntas frequentes sobre thiserror e anyhow

Qual a diferença entre thiserror e anyhow em Rust?

thiserror gera implementações para enums e structs de erro que continuam sendo tipos concretos. anyhow oferece um contêiner flexível (anyhow::Error) para propagar causas diferentes e acrescentar contexto. Em resumo: thiserror ajuda a modelar erros; anyhow ajuda a transportar e relatar erros.

Quando devo usar thiserror?

Use thiserror quando outra parte do programa precisa fazer match no erro, quando a falha faz parte da API pública ou quando você precisa mapear variantes para respostas diferentes. Bibliotecas, domínio, repositórios e serviços são os casos mais comuns.

Quando devo usar anyhow?

Use anyhow em aplicações, CLIs, jobs e funções de bootstrap nas quais a falha será registrada, exibida e encerrará a operação. O método .context() é especialmente útil para explicar qual arquivo, pedido ou etapa estava sendo processado.

Posso usar thiserror e anyhow juntos?

Sim. Esse costuma ser o desenho mais equilibrado: erros tipados com thiserror nas camadas internas e anyhow::Result nas bordas. Assim o domínio permanece explícito, enquanto o executável ganha ergonomia e mensagens de diagnóstico melhores.

O operador ? funciona com as duas crates?

Sim. No thiserror, #[from] gera as conversões que o operador ? usa. No anyhow, qualquer erro compatível pode ser convertido para anyhow::Error; depois, .context() ou .with_context() acrescenta informação sem descartar a causa original.

Conclusão

O tratamento de erros em Rust é robusto por design. Com o Result<T, E> como fundação, o thiserror para erros tipados e o anyhow para aplicações, você tem ferramentas para cobrir qualquer cenário — desde bibliotecas públicas até aplicações complexas em produção, incluindo projetos de CLI e microsserviços.

A regra de ouro é: thiserror para bibliotecas, anyhow para aplicações, e contexto sempre. Com essas três diretrizes, seus erros serão informativos, tipados e fáceis de depurar.

Se você quer se aprofundar mais, confira também nosso artigo sobre boas práticas de error handling e o guia completo de Macros em Rust para entender como o derive do thiserror funciona internamente.

Veja Também