Rust Admin

Boas Práticas de Biblioteca thiserror: Erros Ergonômicos em Rust para Times Ágeis Já leu

Entendendo a Biblioteca thiserror A biblioteca é um derive macro que simplifica significativamente a criação de tipos de erro customizados em Rust. Enquanto a forma tradicional de implementar a trait exige boilerplate code verboso, automatiza esse processo, deixando seu código mais legível e mantível. A biblioteca é particularmente valiosa em projetos que lidam com múltiplas fontes de erro, como aplicações web, CLIs e bibliotecas. O objetivo principal é reduzir a fricção na criação de erros ergonômicos. Em vez de escrever manualmente implementações de , e outras traits, você usa atributos declarativos que o derive macro processa em tempo de compilação. Isso segue a filosofia Rust de "segurança sem sacrificar expressividade". Por que não usar std::error::Error diretamente? Implementar manualmente exige boilerplate: implementar , , para conversão automática. Com , tudo fica em um único tipo com atributos descritivos. Compare com — muito mais clean. Estrutura Básica e Sintaxe A biblioteca funciona através de atributos aplicados a enums ou structs. O atributo

Entendendo a Biblioteca thiserror

A biblioteca thiserror é um derive macro que simplifica significativamente a criação de tipos de erro customizados em Rust. Enquanto a forma tradicional de implementar a trait std::error::Error exige boilerplate code verboso, thiserror automatiza esse processo, deixando seu código mais legível e mantível. A biblioteca é particularmente valiosa em projetos que lidam com múltiplas fontes de erro, como aplicações web, CLIs e bibliotecas.

O objetivo principal é reduzir a fricção na criação de erros ergonômicos. Em vez de escrever manualmente implementações de Display, From e outras traits, você usa atributos declarativos que o derive macro processa em tempo de compilação. Isso segue a filosofia Rust de "segurança sem sacrificar expressividade".

Por que não usar std::error::Error diretamente?

Implementar manualmente std::error::Error exige boilerplate: implementar Display, Debug, From para conversão automática. Com thiserror, tudo fica em um único tipo com atributos descritivos.

// Sem thiserror - verboso
use std::error::Error;
use std::fmt;

#[derive(Debug)]
enum MinhaErro {
    IoError(std::io::Error),
    ParseError(String),
}

impl fmt::Display for MinhaErro {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MinhaErro::IoError(e) => write!(f, "Erro de IO: {}", e),
            MinhaErro::ParseError(msg) => write!(f, "Erro de parse: {}", msg),
        }
    }
}

impl Error for MinhaErro {}

impl From<std::io::Error> for MinhaErro {
    fn from(err: std::io::Error) -> Self {
        MinhaErro::IoError(err)
    }
}

Compare com thiserror — muito mais clean.

Estrutura Básica e Sintaxe

A biblioteca funciona através de atributos aplicados a enums ou structs. O atributo #[error(...)] define como cada variante será exibida quando convertida em string, enquanto #[from] gera automaticamente implementações de From.

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("Arquivo não encontrado: {0}")]
    FileNotFound(String),

    #[error("Erro de parsing TOML: {0}")]
    TomlError(#[from] toml::de::Error),

    #[error("Valor inválido para '{key}': {value}")]
    InvalidValue { key: String, value: String },

    #[error("Erro de I/O")]
    IoError(#[from] std::io::Error),
}

Neste exemplo, #[from] permite conversão automática de toml::de::Error e std::io::Error para ConfigError. Quando você retorna ? sobre esses tipos, a conversão acontece implicitamente. O atributo #[error(...)] define exatamente como cada erro será exibido ao usuário.

Usando #[source] para Error Chaining

O atributo #[source] é crucial para criar chains de erro que preservam a causa raiz. Isso permite que ferramentas de diagnóstico e logs rastreiem o caminho completo do erro.

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ProcessingError {
    #[error("Falha ao processar arquivo")]
    ProcessingFailed {
        #[from]
        source: std::io::Error,
    },

    #[error("Validação falhou: {message}")]
    ValidationFailed {
        message: String,
        #[source]
        cause: Box<dyn std::error::Error + Send + Sync>,
    },
}

O #[source] permite que bibliotecas como anyhow e eyre façam backtrace completo dos erros. Quando você imprime o erro com .source(), consegue navegar toda a cadeia.

Padrões Avançados

Combinando com Result Type Alias

Um padrão muito comum é criar um type alias Result para sua aplicação, eliminando a necessidade de sempre escrever Result<T, SeuErro>.

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ApiError {
    #[error("Requisição HTTP falhou: {0}")]
    HttpError(#[from] reqwest::Error),

    #[error("Resposta inválida JSON: {0}")]
    JsonError(#[from] serde_json::Error),

    #[error("Servidor retornou status {code}: {message}")]
    ServerError { code: u16, message: String },
}

pub type ApiResult<T> = Result<T, ApiError>;

// Uso muito mais limpo
pub async fn fetch_user(id: u64) -> ApiResult<User> {
    let response = reqwest::get(&format!("https://api.example.com/users/{}", id)).await?;
    let user = response.json::<User>().await?;
    Ok(user)
}

Agora ApiResult<T> é o idioma padrão do seu código, reduzindo ruído visual e tornando assinaturas de função mais expressivas.

Erros Transparentes com #[error]

Para casos onde você quer que um tipo de erro simplesmente passe através com a mensagem do erro subjacente, use #[error(transparent)].

use thiserror::Error;

#[derive(Error, Debug)]
#[error(transparent)]
pub struct ParseError(#[from] std::num::ParseIntError);

// Agora ParseError se comporta exatamente como ParseIntError,
// apenas adicionando segurança de tipo
fn parse_number(s: &str) -> Result<i32, ParseError> {
    Ok(s.parse()?)
}

Conclusão

A biblioteca thiserror resolve um problema real de ergonomia em Rust: criação de erros customizados sem boilerplate excessivo. Três aprendizados principais: primeiro, o derive macro #[derive(Error)] + atributo #[error(...)] elimina a necessidade de implementar Display manualmente; segundo, #[from] fornece conversão automática entre tipos de erro, permitindo usar ? livremente; terceiro, #[source] preserva a cadeia de erro para debugging eficaz, especialmente importante em aplicações production-grade.

Referências


Artigos relacionados