Tratamento de erros em Rust: de unwrap a técnicas avançadas
Ao contrário de linguagens com exceções (Java, Python, C++), Rust não utiliza o mecanismo try/catch. Em vez disso, a linguagem oferece um sistema de tipos poderoso e seguro para tratamento de erros. Desenvolvedores iniciantes frequentemente abusam de unwrap(), o que leva ao pânico da aplicação. Neste artigo, exploraremos técnicas avançadas de trabalho com erros em Rust: desde a criação de tipos de erro personalizados até o uso das crates thiserror e anyhow, além de analisar como emular o comportamento de try/catch.
1. Por que o Rust abandonou o try/catch?
Em linguagens com exceções, o fluxo de execução pode ser interrompido a qualquer momento. Isso dificulta a análise do código e pode levar a vazamentos de recursos. Rust oferece duas abordagens principais:
- Erros recuperáveis — o tipo
Result<T, E>(sucessoOk(T)ou erroErr(E)). - Erros irrecuperáveis — a macro
panic!, que interrompe o programa.
O operador ? é o equivalente do Rust ao try. Ele extrai o valor de Ok ou retorna imediatamente o Err da função.
use std::fs::File;use std::io::Read;
fn read_username(path: &str) -> Result<String, std::io::Error> { let mut file = File::open(path)?; // se erro — retorna-o let mut username = String::new(); file.read_to_string(&mut username)?; // novamente "try" Ok(username.trim().to_string())}2. Criando tipos de erro personalizados
Para projetos complexos, apenas std::io::Error não é suficiente. A melhor prática é definir seu próprio tipo de erro usando uma enumeração (enum). Isso permite agrupar diferentes tipos de erros e adicionar contexto.
use std::fmt;
#[derive(Debug)]enum MyAppError { IoError(std::io::Error), ParseError(String), NetworkError { url: String, status: u16 },}
impl fmt::Display for MyAppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { MyAppError::IoError(e) => write!(f, "Erro de IO: {}", e), MyAppError::ParseError(s) => write!(f, "Erro de parsing: {}", s), MyAppError::NetworkError { url, status } => { write!(f, "Erro de rede para {}: status {}", url, status) } } }}
impl std::error::Error for MyAppError {} // implementa o trait ErrorAgora é possível usar ? para conversão automática, se implementar o trait From:
impl From<std::io::Error> for MyAppError { fn from(err: std::io::Error) -> MyAppError { MyAppError::IoError(err) }}
fn process_file(path: &str) -> Result<(), MyAppError> { let content = std::fs::read_to_string(path)?; // converte automaticamente if content.is_empty() { return Err(MyAppError::ParseError("arquivo vazio".into())); } Ok(())}3. Crates avançadas: thiserror e anyhow
Escrever manualmente todas as implementações de Display e From é tedioso. Para isso, existem duas crates populares:
thiserror — para bibliotecas
A crate thiserror gera a implementação de std::error::Error através de uma macro derive. Ideal para bibliotecas, onde é necessária uma descrição precisa dos erros.
use thiserror::Error;
#[derive(Error, Debug)]pub enum DataError { #[error("Erro de IO: {0}")] Io(#[from] std::io::Error), #[error("UTF-8 inválido: {0}")] Utf8(#[from] std::str::Utf8Error), #[error("Falha personalizada: {details}")] Custom { details: String },}
// Agora é possível escrever:fn parse_data(bytes: &[u8]) -> Result<String, Da