Tratamento de erros idiomático
Esta aula ensina como lidar com falhas em Rust de forma idiomática, contrastando panic! e Result, apresentando o operador ? para propagação de erros, técnicas de conversão e boas práticas para código robusto.
Em Rust, o tratamento de erros é uma parte fundamental da linguagem, projetado para ser explícito e seguro. Diferentemente de muitas linguagens que usam exceções, Rust oferece dois mecanismos principais: panic! para erros irrecuperáveis e Result para erros recuperáveis. Nesta aula, exploraremos como usar esses mecanismos de forma idiomática, incluindo o operador ? para simplificar a propagação de erros, técnicas de conversão entre tipos de erro e as melhores práticas para escrever código robusto e expressivo.
panic! vs Result
O panic! é usado para situações em que o programa encontra um estado inválido ou um bug que não pode ser tratado. Quando ocorre um panic!, a thread atual desenrola a pilha (ou aborta, dependendo da configuração) e o programa termina. Exemplos típicos incluem acesso fora dos limites de um array ou divisão por zero. Por outro lado, Result<T, E> é um enum que representa o sucesso (Ok(T)) ou falha (Err(E)) de uma operação que pode falhar de forma esperada, como abrir um arquivo ou fazer uma requisição HTTP. A escolha entre eles depende da natureza do erro: use panic! para erros que indicam bugs ou condições impossíveis, e Result para erros que são esperados e podem ser tratados pelo chamador.
Exemplo de panic!:
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Divisão por zero!");
}
a / b
}
Exemplo de Result:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("Divisão por zero".to_string())
} else {
Ok(a / b)
}
}
No primeiro caso, se b for zero, o programa entra em pânico e para. No segundo, o chamador recebe um Err e pode decidir como lidar com a situação.
O operador ?
O operador ? é uma forma concisa de propagar erros. Ele pode ser usado apenas em funções que retornam Result ou Option. Quando aplicado a um valor Result, ele desempacota o Ok ou retorna o Err imediatamente da função atual. Isso elimina a necessidade de escrever match explícitos para cada chamada que pode falhar, tornando o código mais limpo e legível.
Exemplo sem ?:
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = match std::fs::File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
}
Com ?:
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = std::fs::File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
Note que o operador ? também funciona com Option: se o valor for None, a função retorna None imediatamente.
Convertendo erros
Frequentemente, uma função que usa ? precisa lidar com diferentes tipos de erro. Por exemplo, uma função que lê um arquivo e faz parsing pode encontrar erros de I/O e erros de parsing. Para usar ? com tipos de erro diferentes, é necessário convertê-los para um tipo comum. Isso é feito implementando o trait From<E> para o tipo de erro alvo. Rust fornece conversões automáticas para tipos como Box<dyn Error> ou String. A crate anyhow e thiserror são populares para simplificar esse processo.
Exemplo usando Box<dyn Error>:
use std::error::Error;
fn process_file(path: &str) -> Result<(), Box<dyn Error>> {
let content = std::fs::read_to_string(path)?; // io::Error convertido automaticamente
let number: i32 = content.trim().parse()?; // ParseIntError convertido
println!("Número: {}", number);
Ok(())
}
Outra abordagem é definir um tipo de erro próprio e implementar From para cada erro externo. Por exemplo:
use std::fmt;
use std::error::Error;
#[derive(Debug)]
pub struct MyError {
details: String,
}
impl MyError {
fn new(msg: &str) -> MyError {
MyError { details: msg.to_string() }
}
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl Error for MyError {}
impl From<std::io::Error> for MyError {
fn from(err: std::io::Error) -> MyError {
MyError::new(&format!("Erro de I/O: {}", err))
}
}
impl From<std::num::ParseIntError> for MyError {
fn from(err: std::num::ParseIntError) -> MyError {
MyError::new(&format!("Erro de parsing: {}", err))
}
}
fn process_file(path: &str) -> Result<(), MyError> {
let content = std::fs::read_to_string(path)?;
let number: i32 = content.trim().parse()?;
println!("Número: {}", number);
Ok(())
}
Boas práticas
Ao lidar com erros em Rust, algumas práticas ajudam a manter o código claro e robusto:
- Use
panic!apenas para bugs ou condições impossíveis: Se um erro pode ser tratado, prefiraResult.panic!deve ser reservado para situações comounwrap()em umOptionque você sabe que éSome(mas prefiraexpect()com uma mensagem descritiva). - Propague erros com
?: Evitematchdesnecessários; use o operador?para delegar o tratamento ao chamador. - Defina tipos de erro personalizados para bibliotecas: Em vez de usar
StringouBox<dyn Error>, crie um enum que represente os possíveis erros da sua API. Isso permite que os usuários tratem erros específicos. - Use crates como
thiserroreanyhow:thiserrorfacilita a definição de tipos de erro com anotações, enquantoanyhowé útil para aplicações que não precisam de tipos de erro específicos, fornecendoanyhow::Resulteanyhow::Error. - Documente os erros: Em funções que retornam
Result, documente quais erros podem ocorrer e em que circunstâncias.
Exemplo de uso de thiserror:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("Erro de I/O: {0}")]
Io(#[from] std::io::Error),
#[error("Erro de parsing: {0}")]
Parse(#[from] std::num::ParseIntError),
}
fn process_file(path: &str) -> Result<(), MyError> {
let content = std::fs::read_to_string(path)?;
let number: i32 = content.trim().parse()?;
println!("Número: {}", number);
Ok(())
}
Observações finais
O tratamento de erros em Rust é projetado para ser explícito e seguro, incentivando o programador a considerar todos os caminhos de falha. Dominar o uso de panic!, Result, o operador ? e a conversão de erros é essencial para escrever código Rust idiomático e confiável. Lembre-se de que a escolha entre panic! e Result depende da semântica do erro: erros recuperáveis devem ser modelados com Result, enquanto erros irrecuperáveis (bugs) podem usar panic!. Sempre que possível, prefira propagar erros para que o chamador decida como lidar com eles.
Referências
- The Rust Programming Language - Capítulo 9: Tratamento de Erros
- Rust by Example - Tratamento de Erros
- Documentação oficial de Result
- Documentação oficial de panic!
- Crate thiserror
- Crate anyhow
Exercícios
-
Crie uma função que lê um arquivo e retorna seu conteúdo como
StringusandoResult. Se o arquivo não existir, retorne um erro personalizado.✓ Resposta:use std::fs::File; use std::io::{self, Read}; fn read_file(path: &str) -> Result<String, io::Error> { let mut file = File::open(path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } -
Escreva uma função que divide dois números e retorna
Result<i32, String>. Use o operador?para propagar o erro de uma função auxiliar que valida a divisão.✓ Resposta:fn validate_division(a: i32, b: i32) -> Result<(), String> { if b == 0 { Err("Divisão por zero".to_string()) } else { Ok(()) } } fn divide(a: i32, b: i32) -> Result<i32, String> { validate_division(a, b)?; Ok(a / b) } -
Defina um enum
MyErrorque possa representar erros de I/O e erros de parsing. ImplementeFrompara ambos os tipos e use em uma função que lê um número de um arquivo.✓ Resposta:use std::error::Error; use std::fmt; #[derive(Debug)] pub enum MyError { Io(std::io::Error), Parse(std::num::ParseIntError), } impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { MyError::Io(e) => write!(f, "I/O error: {}", e), MyError::Parse(e) => write!(f, "Parse error: {}", e), } } } impl Error for MyError {} impl From<std::io::Error> for MyError { fn from(e: std::io::Error) -> MyError { MyError::Io(e) } } impl From<std::num::ParseIntError> for MyError { fn from(e: std::num::ParseIntError) -> MyError { MyError::Parse(e) } } fn read_number_from_file(path: &str) -> Result<i32, MyError> { let content = std::fs::read_to_string(path)?; let number = content.trim().parse()?; Ok(number) } -
Usando a crate
thiserror, reescreva o enumMyErrordo exercício anterior de forma mais concisa.✓ Resposta:use thiserror::Error; #[derive(Error, Debug)] pub enum MyError { #[error("I/O error: {0}")] Io(#[from] std::io::Error), #[error("Parse error: {0}")] Parse(#[from] std::num::ParseIntError), } fn read_number_from_file(path: &str) -> Result<i32, MyError> { let content = std::fs::read_to_string(path)?; let number = content.trim().parse()?; Ok(number) } -
Escreva um programa que tenta abrir um arquivo, lê seu conteúdo e imprime. Se o arquivo não existir, imprima uma mensagem amigável e saia. Use
panic!ouResultconforme apropriado.✓ Resposta:use std::fs::File; use std::io::{self, Read}; fn main() { match read_file_and_print("example.txt") { Ok(()) => println!("Sucesso!"), Err(e) => eprintln!("Erro ao ler arquivo: {}", e), } } fn read_file_and_print(path: &str) -> Result<(), io::Error> { let mut file = File::open(path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; println!("Conteúdo: {}", contents); Ok(()) }