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.