Por Que Tratamento de Erros em Rust é Diferente
Em Rust, erros não são exceções que explodem silenciosamente. O sistema de tipos força você a lidar com eles explicitamente através de Result<T, E>. A biblioteca anyhow surge para resolver um problema real: quando você tem múltiplas fontes de erro em uma aplicação, é cansativo definir tipos de erro customizados para cada contexto. anyhow oferece um tipo de erro genérico e flexível que funciona como um "coringa" para tratamento de erros, permitindo que você se concentrate na lógica da sua aplicação em vez de criar boilerplate.
Diferente das exceções tradicionais, com anyhow você mantém segurança em tempo de compilação enquanto reduz complexidade. A biblioteca é ideal para aplicações, CLIs e servidores onde você quer retornar erros sem criar uma hierarquia elaborada de tipos customizados.
Fundamentos: Result, Error Traits e anyhow
Entendendo o tipo Result
Result<T, E> é o coração do tratamento de erros em Rust. Ele força o programador a reconhecer que uma operação pode falhar:
use std::fs;
fn ler_arquivo(caminho: &str) -> Result<String, std::io::Error> {
fs::read_to_string(caminho)
}
O problema: quando você mistura operações que retornam diferentes tipos de erro (io::Error, json::Error, ParseIntError), fica complexo gerenciar tudo num único tipo E.
Introduzindo anyhow
anyhow fornece o tipo Result<T, anyhow::Error>, que encapsula qualquer erro que implemente o trait std::error::Error. Adicione ao seu Cargo.toml:
[dependencies]
anyhow = "1.0"
Agora você pode fazer isto:
use anyhow::Result;
use std::fs;
use std::num::ParseIntError;
fn processar_dados(caminho: &str) -> Result<i32> {
let conteudo = fs::read_to_string(caminho)?; // io::Error convertido automaticamente
let numero: i32 = conteudo.trim().parse()?; // ParseIntError convertido automaticamente
Ok(numero * 2)
}
fn main() {
match processar_dados("dados.txt") {
Ok(valor) => println!("Resultado: {}", valor),
Err(e) => eprintln!("Erro: {}", e),
}
}
O operador ? faz a conversão automática usando o trait From<E> for anyhow::Error. Você não precisa definir conversões entre tipos de erro — anyhow cuida disso.
Padrões Práticos: Contexto, Logging e Debugging
Adicionando Contexto aos Erros
Erros genéricos perdem contexto. anyhow oferece .context() para adicionar informação útil:
use anyhow::{Context, Result};
use std::fs;
fn carregar_config(caminho: &str) -> Result<String> {
fs::read_to_string(caminho)
.context(format!("Falha ao ler arquivo de configuração: {}", caminho))?;
let config: serde_json::Value = serde_json::from_str(&conteudo)
.context("Configuração JSON inválida")?;
Ok(conteudo)
}
Quando o erro se propaga até o main, você vê uma corrente legível de "Por que falhou?" em vez de apenas "arquivo não encontrado".
Convertendo Erros Customizados
Se você já tem tipos de erro customizados, anyhow integra facilmente:
use anyhow::{anyhow, Result};
#[derive(Debug)]
enum MeuErro {
ConfigInvalida(String),
ConexaoPerdida,
}
impl std::fmt::Display for MeuErro {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MeuErro::ConfigInvalida(msg) => write!(f, "Config inválida: {}", msg),
MeuErro::ConexaoPerdida => write!(f, "Conexão perdida"),
}
}
}
impl std::error::Error for MeuErro {}
fn conectar() -> Result<String> {
Err(anyhow!(MeuErro::ConexaoPerdida))
}
Você pode converter qualquer erro que implemente std::error::Error para anyhow::Error com anyhow!().
Debugging com Chain de Erros
anyhow permite inspecionar toda a cadeia de causas do erro:
use anyhow::Result;
fn processar_requisicao() -> Result<()> {
// Simula erro aninhado
let resultado = (|| -> Result<()> {
Err(anyhow::anyhow!("Erro interno"))
})()?;
Ok(())
}
fn main() {
if let Err(e) = processar_requisicao() {
eprintln!("Erro raiz: {}", e);
// Inspeciona chain completa
for causa in e.chain().skip(1) {
eprintln!(" Causado por: {}", causa);
}
}
}
Casos de Uso Avançados: CLI e Servidores Web
Aplicações de Linha de Comando
Em CLIs, você quer erros legíveis para o usuário final:
use anyhow::Result;
use std::env;
use std::fs;
fn main() {
if let Err(e) = executar() {
eprintln!("erro: {}", e);
std::process::exit(1);
}
}
fn executar() -> Result<()> {
let arquivo = env::args()
.nth(1)
.ok_or_else(|| anyhow::anyhow!("Use: app <arquivo>"))?;
let conteudo = fs::read_to_string(&arquivo)
.map_err(|e| anyhow::anyhow!("Não consegui ler '{}': {}", arquivo, e))?;
println!("{}", conteudo);
Ok(())
}
Em Frameworks Web (Actix/Axum)
Muitos frameworks suportam anyhow::Result nativamente:
use anyhow::Result;
use actix_web::{web, HttpResponse};
async fn buscar_usuario(id: web::Path<u32>) -> Result<HttpResponse> {
let dados = serde_json::json!({"id": id.into_inner(), "nome": "Alice"});
Ok(HttpResponse::Ok().json(dados))
}
O framework converte anyhow::Error para uma resposta HTTP apropriada.
Conclusão
Três aprendizados fundamentais: Primeiro, anyhow elimina boilerplate de tipos de erro customizados, mantendo segurança em tempo de compilação — use quando você não precisa tratar tipos de erro diferentes de forma específica. Segundo, .context() é seu aliado para transformar erros genéricos em mensagens úteis, economizando horas de debugging. Terceiro, para aplicações reais (CLIs, APIs, scripts), anyhow::Result é a escolha padrão; reserve tipos customizados para bibliotecas reutilizáveis onde o consumidor precisa diferenciar tipos de erro.
Comece pequeno: substitua unwrap() por ? com anyhow::Result e adicione .context() conforme necessário. Você verá erros mais claros imediatamente.