Introdução
O tratamento de erros é um dos pontos fortes do Rust — o sistema de tipos com Result<T, E> e a ausência de exceções forçam o desenvolvedor a lidar com erros de forma explícita. Porém, implementar tipos de erro customizados “na mão” pode ser trabalhoso.
Três bibliotecas surgiram para resolver esse problema, cada uma com uma filosofia diferente:
thiserror— facilita a criação de tipos de erro customizados para bibliotecas, gerando automaticamente implementações destd::error::ErroreDisplayanyhow— fornece um tipo de erro genérico para aplicações, permitindo propagar qualquer erro com contexto adicionalmiette— estende o tratamento de erros com diagnósticos ricos e formatação visual bonita, ideal para CLIs e ferramentas de desenvolvimento
As três foram criadas por membros proeminentes da comunidade Rust e são amplamente utilizadas em produção.
Tabela Comparativa
| Característica | thiserror | anyhow | miette |
|---|---|---|---|
| Autor | David Tolnay | David Tolnay | Kat Marchán |
| Uso principal | Library crates | Application crates | CLIs e ferramentas |
| Tipo de erro | Customizado (enum/struct) | anyhow::Error genérico | miette::Report com diagnósticos |
| Derive macro | #[derive(Error)] | N/A | #[derive(Diagnostic)] |
| Contexto | Via #[from] e #[source] | .context() e .with_context() | Spans, labels, help, URL |
| Pretty print | Não | Não (básico) | Sim (formatação rica) |
| Performance | Zero-cost (compile-time) | Mínimo overhead | Overhead para diagnósticos |
| Backtrace | Via std | Sim (automático) | Sim |
Dependências no Cargo.toml
[dependencies]
thiserror = "2" # Para tipos de erro customizados
anyhow = "1" # Para propagação genérica de erros
miette = { version = "7", features = ["fancy"] } # Para diagnósticos ricos
thiserror: Erros Customizados para Bibliotecas
O thiserror usa derive macros para gerar implementações de std::error::Error, Display e From:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("Usuário não encontrado: {id}")]
UsuarioNaoEncontrado { id: u64 },
#[error("Credenciais inválidas para o usuário '{usuario}'")]
CredenciaisInvalidas { usuario: String },
#[error("Erro de banco de dados")]
Banco(#[from] sqlx::Error),
#[error("Erro de I/O: {0}")]
Io(#[from] std::io::Error),
#[error("Erro de serialização: {0}")]
Json(#[from] serde_json::Error),
#[error("Valor inválido: {campo} deve ser {regra}")]
Validacao { campo: String, regra: String },
#[error("Erro interno: {0}")]
Interno(String),
}
Uso em Funções
use thiserror::Error;
use std::fs;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Arquivo de configuração não encontrado: {caminho}")]
ArquivoNaoEncontrado { caminho: String },
#[error("Erro ao ler arquivo: {0}")]
Io(#[from] std::io::Error),
#[error("Erro ao fazer parse da configuração: {0}")]
Parse(#[from] toml::de::Error),
#[error("Campo obrigatório ausente: '{campo}'")]
CampoAusente { campo: String },
}
#[derive(serde::Deserialize)]
struct Config {
host: Option<String>,
porta: Option<u16>,
}
fn carregar_config(caminho: &str) -> Result<Config, ConfigError> {
if !std::path::Path::new(caminho).exists() {
return Err(ConfigError::ArquivoNaoEncontrado {
caminho: caminho.to_string(),
});
}
let conteudo = fs::read_to_string(caminho)?; // Auto-converte io::Error
let config: Config = toml::from_str(&conteudo)?; // Auto-converte toml::de::Error
if config.host.is_none() {
return Err(ConfigError::CampoAusente {
campo: "host".to_string(),
});
}
Ok(config)
}
Erros com Source Chain
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ServicoError {
#[error("Falha ao processar pedido #{pedido_id}")]
Processamento {
pedido_id: u64,
#[source]
causa: Box<dyn std::error::Error + Send + Sync>,
},
#[error("Timeout ao consultar serviço externo")]
Timeout {
#[source]
source: reqwest::Error,
},
}
anyhow: Propagação Genérica para Aplicações
O anyhow fornece anyhow::Error — um tipo que pode conter qualquer erro, com suporte a contexto:
use anyhow::{Context, Result, bail, ensure, anyhow};
use std::fs;
// Result<T> é alias para Result<T, anyhow::Error>
fn ler_config(caminho: &str) -> Result<Config> {
let conteudo = fs::read_to_string(caminho)
.with_context(|| format!("Falha ao ler arquivo de configuração: {caminho}"))?;
let config: Config = toml::from_str(&conteudo)
.context("Falha ao fazer parse do TOML")?;
// bail! retorna Err imediatamente
if config.porta == 0 {
bail!("Porta não pode ser zero");
}
// ensure! é como assert! mas retorna Err
ensure!(config.porta > 1023, "Porta deve ser maior que 1023, recebido: {}", config.porta);
Ok(config)
}
#[derive(serde::Deserialize)]
struct Config {
host: String,
porta: u16,
}
fn main() -> Result<()> {
let config = ler_config("config.toml")?;
println!("Servidor: {}:{}", config.host, config.porta);
Ok(())
}
Contexto em Cadeias de Erros
use anyhow::{Context, Result};
async fn iniciar_servidor() -> Result<()> {
let config = carregar_config()
.context("Falha ao carregar configuração")?;
let pool = conectar_banco(&config.database_url)
.await
.context("Falha ao conectar ao banco de dados")?;
let listener = tokio::net::TcpListener::bind(&config.endereco)
.await
.with_context(|| format!("Falha ao vincular na porta {}", config.porta))?;
println!("Servidor iniciado em {}", config.endereco);
Ok(())
}
async fn carregar_config() -> Result<AppConfig> {
// ...
todo!()
}
async fn conectar_banco(url: &str) -> Result<()> {
// ...
todo!()
}
struct AppConfig {
database_url: String,
endereco: String,
porta: u16,
}
Quando ocorre um erro, o anyhow mostra a cadeia completa de contexto:
Error: Falha ao carregar configuração
Caused by:
0: Falha ao fazer parse do TOML
1: expected value, found eof at line 3 column 1
Downcast para Tipos Específicos
use anyhow::Result;
use std::io;
fn processar() -> Result<()> {
// ... pode gerar vários tipos de erro
todo!()
}
fn main() {
match processar() {
Ok(_) => println!("Sucesso"),
Err(e) => {
// Tentar fazer downcast para tipo específico
if let Some(io_err) = e.downcast_ref::<io::Error>() {
eprintln!("Erro de I/O: {io_err}");
} else {
eprintln!("Erro: {e:#}"); // {:#} mostra a cadeia completa
}
}
}
}
miette: Diagnósticos Ricos para CLIs
O miette é ideal quando você quer mostrar erros bonitos e informativos, como compiladores e ferramentas de desenvolvimento fazem:
use miette::{Diagnostic, SourceSpan, NamedSource, Result};
use thiserror::Error;
#[derive(Debug, Error, Diagnostic)]
#[error("Erro de sintaxe no arquivo de configuração")]
#[diagnostic(
code(config::syntax_error),
help("Verifique a sintaxe do arquivo. Consulte a documentação em https://exemplo.com/docs"),
url("https://exemplo.com/erros/E001")
)]
struct ErroSintaxe {
#[source_code]
src: NamedSource<String>,
#[label("erro encontrado aqui")]
span: SourceSpan,
#[label("esta seção pode estar relacionada")]
contexto: Option<SourceSpan>,
}
fn validar_config(conteudo: &str, arquivo: &str) -> Result<()> {
// Simular erro na posição 45, tamanho 3
if conteudo.contains("xxx") {
let pos = conteudo.find("xxx").unwrap();
return Err(ErroSintaxe {
src: NamedSource::new(arquivo, conteudo.to_string()),
span: (pos, 3).into(),
contexto: None,
}.into());
}
Ok(())
}
fn main() -> Result<()> {
miette::set_hook(Box::new(|_| {
Box::new(
miette::MietteHandlerOpts::new()
.terminal_links(true)
.unicode(true)
.context_lines(2)
.build(),
)
})).unwrap();
let conteudo = r#"
[servidor]
host = "localhost"
porta = xxx
"#;
validar_config(conteudo, "config.toml")?;
Ok(())
}
Saída visual:
× Erro de sintaxe no arquivo de configuração
╭─[config.toml:4:9]
3 │ host = "localhost"
4 │ porta = xxx
· ─┬─
· ╰── erro encontrado aqui
╰────
help: Verifique a sintaxe do arquivo. Consulte a documentação.
docs: https://exemplo.com/erros/E001
Múltiplos Erros
use miette::{Diagnostic, Report};
use thiserror::Error;
#[derive(Debug, Error, Diagnostic)]
#[error("Erros de validação encontrados")]
struct ErrosValidacao {
#[related]
erros: Vec<ErroValidacao>,
}
#[derive(Debug, Error, Diagnostic)]
#[error("{mensagem}")]
struct ErroValidacao {
mensagem: String,
#[help]
dica: String,
}
fn validar_formulario(nome: &str, email: &str, idade: i32) -> Result<(), Report> {
let mut erros = vec![];
if nome.is_empty() {
erros.push(ErroValidacao {
mensagem: "Nome é obrigatório".to_string(),
dica: "Forneça um nome com pelo menos 2 caracteres".to_string(),
});
}
if !email.contains('@') {
erros.push(ErroValidacao {
mensagem: format!("Email inválido: '{email}'"),
dica: "O email deve conter @ e um domínio válido".to_string(),
});
}
if idade < 0 || idade > 150 {
erros.push(ErroValidacao {
mensagem: format!("Idade inválida: {idade}"),
dica: "A idade deve estar entre 0 e 150".to_string(),
});
}
if erros.is_empty() {
Ok(())
} else {
Err(ErrosValidacao { erros }.into())
}
}
Combinando as Três: Padrão Recomendado
O padrão mais comum em projetos Rust é:
thiserrorpara definir tipos de erro internos da bibliotecaanyhowna camada de aplicação para propagação fácilmiettequando a CLI precisa de output rico
// src/errors.rs — tipos de erro com thiserror
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DomainError {
#[error("Entidade não encontrada: {tipo} com id {id}")]
NaoEncontrado { tipo: String, id: String },
#[error("Operação não autorizada")]
NaoAutorizado,
#[error("Dados inválidos: {0}")]
Validacao(String),
}
// src/service.rs — lógica de negócio retorna DomainError
pub fn buscar_usuario(id: &str) -> Result<String, DomainError> {
if id == "0" {
return Err(DomainError::NaoEncontrado {
tipo: "usuario".to_string(),
id: id.to_string(),
});
}
Ok(format!("Usuario-{id}"))
}
// src/main.rs — aplicação usa anyhow para composição
use anyhow::{Context, Result};
mod errors;
mod service;
fn main() -> Result<()> {
let usuario = service::buscar_usuario("42")
.context("Falha ao buscar usuário para processar pedido")?;
println!("Usuário encontrado: {usuario}");
Ok(())
}
Integração com Frameworks Web
Com Axum
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use thiserror::Error;
use serde_json::json;
#[derive(Debug, Error)]
pub enum ApiError {
#[error("Recurso não encontrado")]
NaoEncontrado,
#[error("Credenciais inválidas")]
NaoAutorizado,
#[error("Dados inválidos: {0}")]
Validacao(String),
#[error("Erro interno")]
Interno(#[from] anyhow::Error),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, mensagem) = match &self {
ApiError::NaoEncontrado => (StatusCode::NOT_FOUND, self.to_string()),
ApiError::NaoAutorizado => (StatusCode::UNAUTHORIZED, self.to_string()),
ApiError::Validacao(_) => (StatusCode::BAD_REQUEST, self.to_string()),
ApiError::Interno(e) => {
tracing::error!("Erro interno: {e:#}");
(StatusCode::INTERNAL_SERVER_ERROR, "Erro interno do servidor".to_string())
}
};
let body = Json(json!({
"erro": mensagem,
"status": status.as_u16(),
}));
(status, body).into_response()
}
}
Quando Usar Cada Biblioteca
Use thiserror quando:
- Está escrevendo uma library crate
- Precisa de tipos de erro específicos e tipados
- Quer que consumidores possam fazer match nos seus erros
- Precisa implementar
Frompara conversão automática com?
Use anyhow quando:
- Está escrevendo uma aplicação (não uma biblioteca)
- Não precisa que o chamador faça match em tipos de erro específicos
- Quer propagação fácil com
.context() - Tem muitas fontes de erro diferentes e quer um tipo unificado
Use miette quando:
- Está construindo uma CLI ou ferramenta de desenvolvimento
- Quer output visual bonito para erros
- Precisa apontar para localizações específicas no código/configuração do usuário
- Quer fornecer dicas e links para documentação junto com erros
Diagrama de decisão:
Está escrevendo uma library?
├── Sim → use thiserror
└── Não → é uma CLI com output rico?
├── Sim → use miette (+ thiserror para definições)
└── Não → use anyhow (+ thiserror para tipos internos)
Conclusão
O ecossistema de error handling do Rust é maduro e ergonômico. A combinação de thiserror para definição de tipos e anyhow para propagação na aplicação cobre a maioria dos casos. Para CLIs e ferramentas, o miette adiciona diagnósticos visuais impressionantes. Dominar essas três bibliotecas é essencial para escrever Rust idiomático e manutenível.