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 deDisplay{0},{nome}— interpolação de campos na mensagem#[from]— implementaFrom<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 paraResult<T, anyhow::Error>.context("mensagem")— adiciona contexto humano ao errobail!("msg")— retorna erro imediatamente (comoreturn Err(...))ensure!(condição, "msg")— comoassert!, mas retornaResultanyhow!("msg")— cria umanyhow::Errorad 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:
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
| 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 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