Rust Admin

Guia Completo de Criando Tipos de Erro Customizados em Rust Já leu

Por que Customizar Tipos de Erro em Rust? Em Rust, tratamento de erros é uma parte fundamental do design da linguagem. O tipo padrão força você a lidar com possíveis falhas explicitamente, o que evita bugs silenciosos. No entanto, usar apenas 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: Conversão Automática Entre Tipos

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.

Referências


Artigos relacionados