Tratamento de erros idiomático
Nesta aula, você aprenderá a tratar erros em Rust de forma idiomática, comparando panic! e Result, dominando o operador ?, convertendo erros entre tipos e aplicando boas práticas para código robusto e legível.
Em Rust, o tratamento de erros é uma parte fundamental da linguagem, projetado para ser seguro e expressivo. Diferente de muitas linguagens que usam exceções, Rust oferece dois caminhos principais: panic! para erros irrecuperáveis e Result para erros recuperáveis. Nesta aula, exploraremos como usar esses mecanismos de forma idiomática, com foco em código claro e manutenível.
panic! vs Result
O macro panic! é usado para situações em que o programa encontra um erro do qual não pode se recuperar, como um índice de array fora dos limites ou uma falha de invariante lógica. Quando panic! é chamado, o programa imprime uma mensagem de erro, desenrola a pilha (opcionalmente) e termina. Exemplo:
fn main() {
let v = vec![1, 2, 3];
v[99]; // isso causa panic!
}
Por outro lado, Result<T, E> é um enum que representa o sucesso (Ok(T)) ou falha (Err(E)) de uma operação. É usado para erros esperados e recuperáveis, como falha ao abrir um arquivo ou ao fazer parsing de uma string. Exemplo:
use std::fs::File;
fn open_file(path: &str) -> Result<File, std::io::Error> {
File::open(path)
}
A escolha entre panic! e Result depende da gravidade do erro. Use panic! para bugs ou estados impossíveis; use Result para erros que o chamador pode e deve tratar.
O operador ?
O operador ? é uma forma concisa de propagar erros. Ele pode ser usado em funções que retornam Result ou Option. Se o valor for Ok ou Some, ele extrai o valor interno; caso contrário, retorna o erro ou None prematuramente. Exemplo:
use std::fs::File;
use std::io::{self, Read};
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // se Err, retorna o erro
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
O operador ? só pode ser usado em funções que retornam Result, Option ou tipos que implementam FromResidual. Ele reduz drasticamente o código boilerplate de match e torna o fluxo mais limpo.
Convertendo erros
Frequentemente, precisamos converter um tipo de erro em outro. Rust usa traits como From e Into para isso. Por exemplo, ao combinar operações que retornam std::io::Error e std::num::ParseIntError, podemos criar um tipo de erro personalizado e implementar From para cada um. Exemplo:
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;
#[derive(Debug)]
enum MyError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for MyError {
fn from(err: io::Error) -> MyError {
MyError::Io(err)
}
}
impl From<ParseIntError> for MyError {
fn from(err: ParseIntError) -> MyError {
MyError::Parse(err)
}
}
fn read_and_parse(path: &str) -> Result<i32, MyError> {
let mut file = File::open(path)?; // converte io::Error em MyError
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let num = contents.trim().parse::<i32>()?; // converte ParseIntError
Ok(num)
}
O operador ? usa From automaticamente para converter o erro para o tipo de retorno. Também podemos usar map_err para conversões explícitas.
Boas práticas
Ao tratar erros em Rust, siga estas boas práticas:
- Prefira
Resultapanic!para erros que podem ser tratados pelo chamador.panic!é para bugs ou situações irremediáveis. - Use o operador
?para propagar erros de forma concisa, a menos que precise de tratamento adicional. - Crie tipos de erro personalizados usando enumerações, implementando
DisplayeError(do traitstd::error::Error). Isso facilita a depuração e o uso de bibliotecas comoanyhowethiserror. - Não engula erros com
unwrap()ouexpect()em código de produção. Use-os apenas em protótipos ou quando o erro é impossível. - Documente os erros que sua função pode retornar, especialmente em APIs públicas.
- Considere usar crates como
anyhowpara erros ad-hoc em aplicações, ethiserrorpara bibliotecas que precisam de tipos de erro bem definidos.
Exemplo de tipo de erro com Display e Error:
use std::fmt;
#[derive(Debug)]
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, "IO error: {}", e),
MyError::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
impl std::error::Error for MyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
MyError::Io(e) => Some(e),
MyError::Parse(e) => Some(e),
}
}
}
Referências
- The Rust Programming Language - Error Handling
- Rust by Example - Error Handling
- std::result - Rust Documentation
- std::option - Rust Documentation
- std::panic! - Rust Documentation
- std::error - Rust Documentation
- anyhow crate
- thiserror crate
Exercícios
-
Escreva uma função que divide dois números, retornando
Result<f64, String>. Se o divisor for zero, retorne um erro com a mensagem "division by zero".✓ Resposta:fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err(String::from("division by zero")) } else { Ok(a / b) } } -
Use o operador ? para reescrever a função abaixo de forma mais concisa:
fn read_file(path: &str) -> Result<String, std::io::Error> { let result = std::fs::read_to_string(path); match result { Ok(s) => Ok(s), Err(e) => Err(e), } }✓ Resposta:fn read_file(path: &str) -> Result<String, std::io::Error> { std::fs::read_to_string(path) } -
Crie um tipo de erro personalizado
MyErrorque possa conter umio::Errorou umParseIntError, e implementeFrompara ambos. Depois, escreva uma função que leia um arquivo, parseie um número e retorne o número comoi32, usando?.✓ Resposta:use std::fs; use std::io; use std::num::ParseIntError; #[derive(Debug)] enum MyError { Io(io::Error), Parse(ParseIntError), } impl From<io::Error> for MyError { fn from(err: io::Error) -> MyError { MyError::Io(err) } } impl From<ParseIntError> for MyError { fn from(err: ParseIntError) -> MyError { MyError::Parse(err) } } fn read_and_parse(path: &str) -> Result<i32, MyError> { let contents = fs::read_to_string(path)?; let num = contents.trim().parse::<i32>()?; Ok(num) } -
Explique por que é considerado má prática usar
unwrap()em código de produção e dê um exemplo de como substituí-lo por um tratamento adequado.✓ Resposta:unwrap()causapanic!se o valor forErrouNone, o que pode derrubar o programa inesperadamente. Em produção, é melhor tratar o erro adequadamente, por exemplo, retornando umResultpara o chamador ou usandomatchpara lidar com cada caso. Exemplo:// Ruim: let file = File::open("foo.txt").unwrap(); // Bom: let file = match File::open("foo.txt") { Ok(f) => f, Err(e) => return Err(e), }; -
Implemente uma função que retorne
Result<Vec<String>, io::Error>que leia um arquivo linha por linha e retorne um vetor com as linhas (ignorando linhas vazias). Use o operador ?.✓ Resposta:use std::fs::File; use std::io::{self, BufRead, BufReader}; fn read_lines(path: &str) -> Result<Vec<String>, io::Error> { let file = File::open(path)?; let reader = BufReader::new(file); let mut lines = Vec::new(); for line in reader.lines() { let line = line?; if !line.is_empty() { lines.push(line); } } Ok(lines) }