Por que Customizar Tipos de Erro em Rust?
Em Rust, tratamento de erros é uma parte fundamental do design da linguagem. O tipo padrão Result<T, E> força você a lidar com possíveis falhas explicitamente, o que evita bugs silenciosos. No entanto, usar apenas String ou tipos genéricos não captura informações semânticas sobre o que deu errado. Tipos de erro customizados permitem que você defina erros específicos do seu domínio, tornando o código mais legível, mantível e type-safe.
Quando você cria tipos de erro próprios, pode implementar conversões automáticas, adicionar contexto rico, e fazer pattern matching sobre falhas específicas. Isso transforma tratamento de erros de uma tarefa incômoda em uma ferramenta poderosa para comunicar falhas e recuperar-se delas de forma elegante.
Criando Seu Primeiro Tipo de Erro Customizado
O Básico: Uma Enumeração com derive
A forma mais direta de criar um tipo de erro customizado é usar uma enumeração. Aqui está um exemplo prático de uma biblioteca de operações matemáticas:
#[derive(Debug)]
enum MathError {
DivisionByZero,
NegativeSqrt(f64),
InvalidInput(String),
}
impl std::fmt::Display for MathError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MathError::DivisionByZero => write!(f, "Divisão por zero não é permitida"),
MathError::NegativeSqrt(n) => write!(f, "Não é possível tirar raiz quadrada de {}", n),
MathError::InvalidInput(msg) => write!(f, "Entrada inválida: {}", msg),
}
}
}
impl std::error::Error for MathError {}
fn divide(a: f64, b: f64) -> Result<f64, MathError> {
if b == 0.0 {
Err(MathError::DivisionByZero)
} else {
Ok(a / b)
}
}
fn square_root(n: f64) -> Result<f64, MathError> {
if n < 0.0 {
Err(MathError::NegativeSqrt(n))
} else {
Ok(n.sqrt())
}
}
Note que implementamos Display e Error. O trait Display é obrigatório para qualquer tipo que implemente Error. Isso permite que erros sejam convertidos em strings legíveis. O derive #[derive(Debug)] já vem pronto — é essencial para debugging.
Usando o Operador ? com Seus Erros
O operador ? torna o código conciso. Quando você retorna ? em uma função que retorna um Result, o erro é automaticamente convertido e propagado:
fn calculate(a: f64, b: f64) -> Result<f64, MathError> {
let quotient = divide(a, b)?;
let result = square_root(quotient)?;
Ok(result)
}
fn main() {
match calculate(16.0, 2.0) {
Ok(value) => println!("Resultado: {}", value),
Err(e) => println!("Erro: {}", e),
}
}
Conversão Automática Entre Tipos de Erro
O Trait From para Conversão Implícita
Quando sua aplicação envolve múltiplos tipos de erro (seus erros customizados + erros de I/O, parsing, etc.), você precisa de conversão automática. O trait From permite que o operador ? converta erros transparentemente:
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Math(MathError),
Io(io::Error),
ParseInt(ParseIntError),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::Math(e) => write!(f, "Erro matemático: {}", e),
AppError::Io(e) => write!(f, "Erro de I/O: {}", e),
AppError::ParseInt(e) => write!(f, "Erro ao fazer parsing: {}", e),
}
}
}
impl std::error::Error for AppError {}
impl From<MathError> for AppError {
fn from(err: MathError) -> Self {
AppError::Math(err)
}
}
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::ParseInt(err)
}
}
fn read_and_calculate(path: &str, divisor: &str) -> Result<f64, AppError> {
let content = std::fs::read_to_string(path)?;
let divisor_num: f64 = divisor.parse()?;
let value: f64 = content.trim().parse()?;
divide(value, divisor_num).map_err(AppError::Math)
}
Note que implementamos From para cada tipo de erro externo. Agora, quando você usa ? dentro de read_and_calculate, qualquer erro é automaticamente convertido para AppError.
Boas Práticas e Padrões Avançados
Adicionar Contexto e Stack Trace
Erros complexos frequentemente precisam de mais contexto. Uma abordagem prática é armazenar a fonte do erro:
#[derive(Debug)]
struct DetailedMathError {
kind: MathErrorKind,
source: Box<dyn std::error::Error + Send + Sync>,
}
#[derive(Debug)]
enum MathErrorKind {
DivisionByZero,
NegativeSqrt,
}
impl DetailedMathError {
fn new(kind: MathErrorKind, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
DetailedMathError { kind, source }
}
}
impl std::fmt::Display for DetailedMathError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}: {}", self.kind, self.source)
}
}
impl std::error::Error for DetailedMathError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.source.as_ref())
}
}
Usar Bibliotecas Especializadas
Para projetos maiores, considere usar thiserror ou anyhow. A crate thiserror reduz boilerplate drasticamente:
use thiserror::Error;
#[derive(Error, Debug)]
enum MathError {
#[error("Divisão por zero não é permitida")]
DivisionByZero,
#[error("Não é possível tirar raiz de número negativo: {0}")]
NegativeSqrt(f64),
#[error("Entrada inválida: {0}")]
InvalidInput(String),
}
Essa abordagem implementa automaticamente Display e Error. O arquivo Cargo.toml precisa incluir: thiserror = "1.0".
Conclusão
Dominar tipos de erro customizados em Rust é essencial para escrever código robusto e idiomático. Os pontos principais são: (1) sempre implemente Display e Error para seus tipos customizados — é a base para integração com o resto do Rust; (2) use From para conversão automática entre tipos de erro, permitindo que o operador ? funcione sem verbosidade; (3) para projetos maiores, prefira crates como thiserror para reduzir boilerplate e melhorar manutenibilidade. Com essas ferramentas, você transforma tratamento de erros de um necessário incômodo em um componente elegante da arquitetura da sua aplicação.