Error handling com Result e o operador ?
1. Introdução ao tratamento de erros em Rust
Rust adota uma filosofia única para tratamento de erros: sem exceções, sem try/catch, sem surpresas em tempo de execução. Tudo é explícito através do sistema de tipos. A linguagem distingue claramente entre erros recuperáveis — representados pelo tipo Result<T, E> — e erros irrecuperáveis — que disparam panic! e geralmente indicam bugs ou estados inválidos.
Enquanto Option<T> lida com a ausência de um valor (Some/None), Result<T, E> carrega informação sobre por que algo falhou. Essa distinção é fundamental para escrever código robusto e previsível.
2. O tipo Result<T, E> em profundidade
Result é um enum com duas variantes:
enum Result<T, E> {
Ok(T),
Err(E),
}
Métodos comuns permitem extrair valores ou verificar o estado:
fn dividir(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("divisão por zero"))
} else {
Ok(a / b)
}
}
let resultado = dividir(10.0, 2.0);
println!("is_ok: {}", resultado.is_ok()); // true
println!("is_err: {}", resultado.is_err()); // false
// Cuidado: unwrap pode causar panic!
let valor = resultado.unwrap(); // 5.0
Combinadores permitem transformar Result sem extrair o valor:
let resultado = dividir(10.0, 0.0)
.map(|v| v * 2.0) // Só executa se Ok
.map_err(|e| format!("Erro: {}", e)); // Transforma o erro
// and_then para encadeamento que pode falhar
fn validar_positivo(x: f64) -> Result<f64, String> {
if x > 0.0 { Ok(x) } else { Err(String::from("não positivo")) }
}
let encadeado = dividir(10.0, 2.0)
.and_then(validar_positivo);
3. Propagação de erros com match
Antes do operador ?, a propagação manual era feita com match:
use std::fs::File;
use std::io::Read;
fn ler_arquivo(path: &str) -> Result<String, std::io::Error> {
let mut arquivo = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut conteudo = String::new();
match arquivo.read_to_string(&mut conteudo) {
Ok(_) => Ok(conteudo),
Err(e) => Err(e),
}
}
Essa abordagem funciona, mas é verbosa. Com múltiplos tipos de erro, o problema se agrava:
fn processar_dados(path: &str) -> Result<i32, String> {
let mut arquivo = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(format!("Erro de IO: {}", e)),
};
let mut conteudo = String::new();
match arquivo.read_to_string(&mut conteudo) {
Ok(_) => {},
Err(e) => return Err(format!("Erro de leitura: {}", e)),
}
let numero: i32 = match conteudo.trim().parse() {
Ok(n) => n,
Err(e) => return Err(format!("Erro de parse: {}", e)),
};
Ok(numero * 2)
}
4. O operador ? para propagação concisa
O operador ? revoluciona a propagação de erros. Ele faz duas coisas:
- Se o valor for Ok(T), extrai T e continua
- Se for Err(E), retorna o erro da função atual (com return implícito)
fn ler_arquivo_simples(path: &str) -> Result<String, std::io::Error> {
let mut arquivo = File::open(path)?;
let mut conteudo = String::new();
arquivo.read_to_string(&mut conteudo)?;
Ok(conteudo)
}
O exemplo anterior com múltiplos erros fica drasticamente mais limpo:
use std::num::ParseIntError;
fn processar_dados_moderno(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let mut arquivo = File::open(path)?;
let mut conteudo = String::new();
arquivo.read_to_string(&mut conteudo)?;
let numero: i32 = conteudo.trim().parse()?;
Ok(numero * 2)
}
5. Conversão de tipos de erro com From
O operador ? não funciona magicamente com tipos diferentes. Ele usa o trait From<E> para converter automaticamente o erro encontrado no tipo de erro esperado pela função. Quando você usa Box<dyn Error>, a conversão é automática porque Box<dyn Error> implementa From para qualquer tipo que implemente Error.
Para erros customizados, implementamos conversões explícitas:
#[derive(Debug)]
enum MeuErro {
Io(std::io::Error),
Parse(ParseIntError),
}
impl From<std::io::Error> for MeuErro {
fn from(err: std::io::Error) -> MeuErro {
MeuErro::Io(err)
}
}
impl From<ParseIntError> for MeuErro {
fn from(err: ParseIntError) -> MeuErro {
MeuErro::Parse(err)
}
}
fn processar_com_erro_customizado(path: &str) -> Result<i32, MeuErro> {
let mut arquivo = File::open(path)?; // Erro IO convertido automaticamente
let mut conteudo = String::new();
arquivo.read_to_string(&mut conteudo)?;
let numero: i32 = conteudo.trim().parse()?; // Erro Parse convertido
Ok(numero * 2)
}
6. Criando tipos de erro customizados
Para bibliotecas ou sistemas complexos, criar seu próprio tipo de erro é essencial:
use std::fmt;
#[derive(Debug)]
enum ErroProcessamento {
ArquivoNaoEncontrado(String),
FormatoInvalido { linha: u32, detalhe: String },
ErroInterno(String),
}
impl fmt::Display for ErroProcessamento {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErroProcessamento::ArquivoNaoEncontrado(path) => {
write!(f, "Arquivo não encontrado: {}", path)
}
ErroProcessamento::FormatoInvalido { linha, detalhe } => {
write!(f, "Formato inválido na linha {}: {}", linha, detalhe)
}
ErroProcessamento::ErroInterno(msg) => {
write!(f, "Erro interno: {}", msg)
}
}
}
}
impl std::error::Error for ErroProcessamento {}
Com a crate thiserror, isso fica muito mais simples:
use thiserror::Error;
#[derive(Error, Debug)]
enum ErroProcessamento {
#[error("Arquivo não encontrado: {0}")]
ArquivoNaoEncontrado(String),
#[error("Formato inválido na linha {linha}: {detalhe}")]
FormatoInvalido { linha: u32, detalhe: String },
#[error("Erro interno: {0}")]
ErroInterno(String),
}
7. Estratégias avançadas com Result
Combinando Result com Option:
fn encontrar_usuario(id: u32) -> Option<String> {
if id == 1 { Some(String::from("Alice")) } else { None }
}
// Convertendo Option em Result com mensagem de erro
let usuario = encontrar_usuario(2)
.ok_or("Usuário não encontrado")?;
Erros em coleções com collect():
let numeros = vec!["10", "20", "trinta", "40"];
let resultados: Result<Vec<i32>, ParseIntError> = numeros
.iter()
.map(|s| s.parse::<i32>())
.collect(); // Para no primeiro erro
match resultados {
Ok(nums) => println!("{:?}", nums),
Err(e) => println!("Erro no parse: {}", e),
}
Para aplicações, anyhow simplifica o tratamento:
use anyhow::{Context, Result};
fn ler_config() -> Result<String> {
let path = "config.toml";
let conteudo = std::fs::read_to_string(path)
.with_context(|| format!("Falha ao ler {}", path))?;
Ok(conteudo)
}
8. Boas práticas e padrões comuns
unwrap()eexpect(): Use apenas em protótipos, testes, ou quando você tem certeza absoluta que o erro não ocorrerá (ex: valores hardcoded).- Operador
?: Preferido para a maioria das funções que podem falhar. Torna o código limpo e explícito. - Tratamento explícito: Use
matchquando precisar de lógica diferente para cada variante de erro. - Função
main(): Pode retornarResult<(), Box<dyn Error>>para usar?diretamente:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let conteudo = std::fs::read_to_string("dados.txt")?;
println!("{}", conteudo);
Ok(())
}
- Documentação: Sempre documente quais erros sua função pode retornar, especialmente em APIs públicas.
- Prefira tipos de erro específicos em bibliotecas (enum customizado) e
anyhowem aplicações.
O tratamento de erros em Rust, embora exija mais planejamento inicial, resulta em código mais seguro, previsível e fácil de debugar. O operador ? combinado com conversões automáticas via From torna a propagação de erros tão concisa quanto em linguagens com exceções, mas com toda a segurança de tipos que Rust oferece.
Referências
- The Rust Programming Language - Chapter 9: Error Handling — Capítulo oficial do livro sobre tratamento de erros, incluindo Result e operador ?
- Rust by Example - Result — Exemplos práticos do tipo Result e seus combinadores
- Rust Reference - The ? operator — Documentação oficial do operador ? na referência da linguagem
- std::result - Rust Standard Library — Documentação completa da API do tipo Result
- thiserror crate documentation — Crate para derivação simplificada do trait Error
- anyhow crate documentation — Crate para tratamento de erros flexível em aplicações
- Error Handling in Rust - A Comprehensive Guide — Artigo técnico detalhado sobre estratégias de tratamento de erros em Rust