Introdução
O tratamento de erros é uma das áreas onde Rust mais se diferencia de outras linguagens. Enquanto linguagens como Python e Java usam exceções e Go retorna tuplas (valor, erro), Rust adota um sistema baseado em tipos com Result<T, E> e Option<T> que força o programador a lidar com cada possível falha em tempo de compilação.
Essa abordagem elimina toda uma classe de bugs — erros não tratados simplesmente não compilam. Porém, escrever código robusto de tratamento de erros vai além de usar Result. Neste artigo, vamos explorar as boas práticas que profissionais experientes aplicam em projetos Rust em produção, desde a criação de tipos de erro customizados até o uso estratégico de crates como thiserror e anyhow.
O Problema: Código com Tratamento de Erros Frágil
Vamos começar com exemplos de como NÃO tratar erros em Rust. Estes padrões são comuns em código de iniciantes e podem causar problemas sérios em produção.
Não Faça Isso: unwrap() em Todo Lugar
use std::fs;
use std::collections::HashMap;
fn carregar_configuracao() -> HashMap<String, String> {
// ERRADO: Se o arquivo não existir, o programa entra em panic
let conteudo = fs::read_to_string("config.toml").unwrap();
// ERRADO: Se o parse falhar, panic novamente
let config: HashMap<String, String> = toml::from_str(&conteudo).unwrap();
config
}
fn main() {
let config = carregar_configuracao();
// ERRADO: Se a chave não existir, panic
let porta = config.get("porta").unwrap();
println!("Servidor na porta: {porta}");
}
Cada .unwrap() é uma bomba-relógio. Se qualquer dessas operações falhar, o programa inteiro entra em panic com uma mensagem genérica como called 'Option::unwrap()' on a 'None' value, sem contexto sobre o que deu errado.
Não Faça Isso: Strings como Tipo de Erro
fn dividir(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
// ERRADO: String como tipo de erro dificulta pattern matching
return Err("Divisão por zero não é permitida".to_string());
}
Ok(a / b)
}
fn calcular() -> Result<f64, String> {
let x = dividir(10.0, 0.0)?;
// Erros de diferentes origens viram uma sopa de strings
let arquivo = std::fs::read_to_string("dados.txt")
.map_err(|e| e.to_string())?;
Ok(x)
}
Usar String como tipo de erro parece simples, mas remove a capacidade de distinguir entre tipos de erros e torna impossível para o código chamador tratar erros específicos de forma diferente.
A Solução: Tipos de Erro Customizados e Propagação Elegante
Faça Isso: Tipos de Erro com thiserror (para Bibliotecas)
Para bibliotecas, use a crate thiserror para criar tipos de erro expressivos e ergonômicos:
# Cargo.toml
[dependencies]
thiserror = "2"
use std::io;
use std::num::ParseIntError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Arquivo de configuração não encontrado: {caminho}")]
ArquivoNaoEncontrado {
caminho: String,
#[source]
fonte: io::Error,
},
#[error("Formato inválido na linha {linha}: {mensagem}")]
FormatoInvalido {
linha: usize,
mensagem: String,
},
#[error("Valor inválido para a porta: {0}")]
PortaInvalida(#[from] ParseIntError),
#[error("Chave obrigatória ausente: {0}")]
ChaveAusente(String),
}
pub fn carregar_configuracao(caminho: &str) -> Result<Config, ConfigError> {
let conteudo = std::fs::read_to_string(caminho)
.map_err(|e| ConfigError::ArquivoNaoEncontrado {
caminho: caminho.to_string(),
fonte: e,
})?;
let porta_str = extrair_valor(&conteudo, "porta")
.ok_or_else(|| ConfigError::ChaveAusente("porta".to_string()))?;
let porta: u16 = porta_str.parse()?; // Usa From<ParseIntError> automaticamente
Ok(Config { porta })
}
pub struct Config {
pub porta: u16,
}
fn extrair_valor<'a>(conteudo: &'a str, chave: &str) -> Option<&'a str> {
for linha in conteudo.lines() {
if let Some(valor) = linha.strip_prefix(&format!("{chave} = ")) {
return Some(valor.trim());
}
}
None
}
Com thiserror, cada variante de erro tem uma mensagem descritiva, pode encapsular a causa raiz (#[source]), e suporta conversão automática (#[from]).
Faça Isso: anyhow para Aplicações
Para aplicações (binários), use anyhow que fornece um tipo de erro genérico com suporte a contexto:
# Cargo.toml
[dependencies]
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use anyhow::{Context, Result, bail};
use serde::Deserialize;
#[derive(Deserialize)]
struct Config {
porta: u16,
host: String,
banco_de_dados: String,
}
fn carregar_configuracao(caminho: &str) -> Result<Config> {
let conteudo = std::fs::read_to_string(caminho)
.with_context(|| format!("Falha ao ler o arquivo de configuração: {caminho}"))?;
let config: Config = serde_json::from_str(&conteudo)
.with_context(|| format!("Falha ao fazer parse do JSON em: {caminho}"))?;
if config.porta == 0 {
bail!("A porta não pode ser zero no arquivo {caminho}");
}
Ok(config)
}
fn main() -> Result<()> {
let config = carregar_configuracao("config.json")
.context("Não foi possível inicializar o servidor")?;
println!("Servidor iniciando em {}:{}", config.host, config.porta);
Ok(())
}
A grande vantagem do anyhow é o método .context() que adiciona camadas de informação ao erro. Quando o erro é exibido, você vê toda a cadeia causal:
Error: Não foi possível inicializar o servidor
Caused by:
0: Falha ao ler o arquivo de configuração: config.json
1: No such file or directory (os error 2)
Guia Passo a Passo: Implementando Error Handling Robusto
Passo 1: Defina a Estratégia — Biblioteca vs Aplicação
A regra de ouro é:
| Tipo de projeto | Crate recomendada | Tipo de erro |
|---|---|---|
| Biblioteca | thiserror | Enum customizada |
| Aplicação | anyhow | anyhow::Result |
| Misto | Ambas | thiserror na API pública, anyhow internamente |
Passo 2: Use o Operador ? para Propagação
O operador ? é a forma idiomática de propagar erros em Rust. Ele extrai o valor de um Result::Ok ou retorna o erro automaticamente:
use anyhow::{Context, Result};
fn ler_numero_do_arquivo(caminho: &str) -> Result<i32> {
let conteudo = std::fs::read_to_string(caminho)
.with_context(|| format!("Não foi possível ler {caminho}"))?;
let numero: i32 = conteudo.trim().parse()
.with_context(|| format!("Conteúdo de {caminho} não é um número válido"))?;
Ok(numero)
}
Passo 3: Use expect() Apenas com Invariantes Lógicos
A diferença entre unwrap() e expect() é que expect() exige uma mensagem descritiva. Mesmo assim, use-o apenas quando a falha representa um bug lógico no programa:
use std::sync::Mutex;
fn main() {
let dados = Mutex::new(vec![1, 2, 3]);
// OK: Se o Mutex está envenenado, é um bug no programa
let guard = dados.lock()
.expect("Mutex envenenado — outro thread entrou em panic");
// ERRADO: Entrada do usuário pode falhar legitimamente
// let porta: u16 = input.parse().expect("Porta inválida");
}
Passo 4: Implemente Display e Error Manualmente (Quando Necessário)
Sem thiserror, você pode implementar os traits manualmente para controle total:
use std::fmt;
use std::error::Error;
#[derive(Debug)]
pub enum AppError {
Database(String),
Validation(String),
NotFound { recurso: String, id: u64 },
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Database(msg) => write!(f, "Erro de banco de dados: {msg}"),
AppError::Validation(msg) => write!(f, "Erro de validação: {msg}"),
AppError::NotFound { recurso, id } => {
write!(f, "{recurso} com id {id} não encontrado")
}
}
}
}
impl Error for AppError {}
Passo 5: Converta Erros com From
Implemente From para converter entre tipos de erros automaticamente (o operador ? usa essa conversão):
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
pub enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::Io(err)
}
}
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> Self {
AppError::Parse(err)
}
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::Io(e) => write!(f, "Erro de I/O: {e}"),
AppError::Parse(e) => write!(f, "Erro de parse: {e}"),
}
}
}
impl std::error::Error for AppError {}
fn processar_arquivo(caminho: &str) -> Result<i32, AppError> {
let conteudo = std::fs::read_to_string(caminho)?; // io::Error → AppError
let numero: i32 = conteudo.trim().parse()?; // ParseIntError → AppError
Ok(numero * 2)
}
Armadilhas Comuns e Como Evitá-las
1. Usar unwrap() em Código Assíncrono
// ERRADO: panic em uma task Tokio pode ser silencioso
async fn buscar_dados(url: &str) -> String {
let resp = reqwest::get(url).await.unwrap(); // Panic silencioso!
resp.text().await.unwrap()
}
// CORRETO: Propague o erro
async fn buscar_dados(url: &str) -> Result<String, reqwest::Error> {
let resp = reqwest::get(url).await?;
let texto = resp.text().await?;
Ok(texto)
}
2. Ignorar Erros com let _ =
// ERRADO: Você está ignorando um possível erro
let _ = std::fs::remove_file("temp.txt");
// MELHOR: Log se falhar, mas continue
if let Err(e) = std::fs::remove_file("temp.txt") {
eprintln!("Aviso: não foi possível remover temp.txt: {e}");
}
3. map_err Sem Contexto Suficiente
// ERRADO: Perde informação sobre o erro original
let conteudo = std::fs::read_to_string("config.toml")
.map_err(|_| "Erro ao ler arquivo")?;
// CORRETO com anyhow: Preserva o erro original e adiciona contexto
use anyhow::Context;
let conteudo = std::fs::read_to_string("config.toml")
.context("Erro ao ler config.toml")?;
4. Panic em Construtores
// ERRADO: new() que pode falhar não deveria usar panic
impl Servidor {
pub fn new(porta: u16) -> Self {
let listener = std::net::TcpListener::bind(format!("0.0.0.0:{porta}"))
.unwrap(); // panic se a porta estiver em uso!
Servidor { listener }
}
}
// CORRETO: Retorne Result para operações que podem falhar
impl Servidor {
pub fn new(porta: u16) -> Result<Self, std::io::Error> {
let listener = std::net::TcpListener::bind(format!("0.0.0.0:{porta}"))?;
Ok(Servidor { listener })
}
}
struct Servidor {
listener: std::net::TcpListener,
}
Exemplos do Mundo Real
API REST com Axum e Tratamento de Erros
# Cargo.toml
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Json, Router,
};
use serde::Serialize;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ApiError {
#[error("Usuário não encontrado: {0}")]
NotFound(u64),
#[error("Erro interno: {0}")]
Internal(String),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, mensagem) = match &self {
ApiError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
ApiError::Internal(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Erro interno do servidor".to_string(),
),
};
let body = serde_json::json!({ "erro": mensagem });
(status, Json(body)).into_response()
}
}
#[derive(Serialize)]
struct Usuario {
id: u64,
nome: String,
}
async fn buscar_usuario(Path(id): Path<u64>) -> Result<Json<Usuario>, ApiError> {
if id == 0 {
return Err(ApiError::NotFound(id));
}
// Simula busca no banco de dados
Ok(Json(Usuario {
id,
nome: "Maria Silva".to_string(),
}))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/usuarios/{id}", get(buscar_usuario));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
CLI com Tratamento de Erros usando anyhow
# Cargo.toml
[dependencies]
anyhow = "1"
use anyhow::{Context, Result, bail};
use std::env;
use std::fs;
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
let caminho = args.get(1)
.context("Uso: programa <caminho-do-arquivo>")?;
let conteudo = fs::read_to_string(caminho)
.with_context(|| format!("Não foi possível ler o arquivo '{caminho}'"))?;
let total_linhas = conteudo.lines().count();
if total_linhas == 0 {
bail!("O arquivo '{caminho}' está vazio");
}
println!("O arquivo '{caminho}' tem {total_linhas} linhas.");
Ok(())
}
Quando main() retorna Result<()>, erros não tratados são exibidos automaticamente no stderr com toda a cadeia de contexto.
Resumo das Boas Práticas
| Prática | Quando Usar |
|---|---|
thiserror | Bibliotecas — erros tipados e expressivos |
anyhow | Aplicações — erros com contexto empilhado |
Operador ? | Sempre — para propagação limpa |
.context() / .with_context() | Sempre — adicione contexto a cada ponto de falha |
expect("motivo") | Apenas para invariantes lógicos |
unwrap() | Apenas em testes e protótipos |
| Pattern matching em erros | Quando você precisa reagir diferente a cada tipo de erro |
Veja Também
- Tutorial: Tratamento de Erros em Rust — Guia completo de Result, Option e o operador ?
- Receita: Tratar Erros em Rust — Exemplos práticos de tratamento de erros
- Erros Comuns: panic e unwrap — Como resolver panics causados por unwrap
- Padrões de Projeto em Rust — Builder, Newtype, Typestate e outros padrões
- Segurança em Rust — Práticas de segurança além da memória
- Logging e Observabilidade em Rust — Como monitorar erros em produção