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ção | Escolha | Motivo |
|---|---|---|
| Crate ou biblioteca pública | thiserror | O consumidor pode fazer match nas variantes |
| Domínio, repositório ou serviço | thiserror | Mantém contratos de erro explícitos e testáveis |
| Binário, CLI, job ou script | anyhow | Simplifica propagação, relatório e contexto |
main, bootstrap e tarefas administrativas | anyhow | A borda normalmente precisa exibir a causa, não classificá-la |
| API HTTP | Ambos | thiserror modela falhas; a borda pode agregar contexto e logs |
| Protótipo pequeno | anyhow | Menos 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 Displayusando as strings do atributo#[error(...)]impl Errorcom a traitstd::error::Errorimpl From<std::io::Error>eimpl 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ário | Recomendação |
|---|---|
| Biblioteca pública (crate) | thiserror — consumidores precisam inspecionar variantes |
| Aplicação binária | anyhow — contexto e relatório são mais importantes |
| Regra de domínio | thiserror — o erro faz parte do contrato |
| API REST | thiserror para classificar falhas e convertê-las em status HTTP |
| Scripts, jobs e CLIs | anyhow — 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
Nunca use
.unwrap()em produção — use.expect("mensagem descritiva")ou trate o erro. Veja nosso guia sobre erros com panic e unwrap.Prefira
?sobrematchquando só precisa propagar o erro.Adicione contexto com
.context()ou.with_context(|| ...)para erros que cruzam fronteiras de módulo.Defina erros tipados para interfaces públicas — consumidores da sua API precisam fazer pattern matching.
Use
#[error(transparent)]quando uma variante de erro é apenas um wrapper.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
- Error Handling: Bibliotecas do Ecossistema
- Iteradores em Rust — combinam perfeitamente com
Resultviacollect() - Serde: Guia Completo — serialização com tratamento de erros
- CI/CD para projetos Rust — automatize a verificação de erros
- Para explorar tratamento de erros em outras linguagens do ecossistema de sistemas, veja também Zig Brasil