Erros customizados
Nesta aula, você aprenderá a criar seus próprios tipos de erro em Rust, implementar a trait Error, e conhecerá as bibliotecas thiserror e anyhow para simplificar o tratamento de erros. O conteúdo aborda desde a definição de erros customizados até boas práticas para aplicações reais.
Em Rust, o tratamento de erros é uma parte fundamental da construção de software robusto. Embora a linguagem forneça tipos de erro padrão como std::io::Error ou std::num::ParseIntError, muitas vezes precisamos de erros que representem domínios específicos da nossa aplicação. Criar tipos de erro customizados permite que você comunique exatamente o que deu errado e forneça informações contextuais úteis.
Nesta aula, vamos explorar como definir seus próprios tipos de erro, implementar a trait Error (que permite integração com o operador ? e com bibliotecas de logging), e dar uma visão geral de duas bibliotecas populares: thiserror (para definir erros de forma concisa) e anyhow (para tratamento flexível de erros em aplicações).
Definindo tipos de erro
Em Rust, a maneira mais comum de definir um tipo de erro é usando um enum, onde cada variante representa uma categoria diferente de erro. Por exemplo, em um processador de arquivos, você pode ter erros de formato inválido, arquivo não encontrado, ou permissão negada. Cada variante pode carregar dados adicionais relevantes ao erro.
Para que o tipo de erro funcione bem com o ecossistema Rust, ele deve implementar as traits Debug e Display. A trait Debug é usada para formatação de depuração (com {:?}), enquanto Display é usada para mensagens amigáveis ao usuário (com {}). Além disso, para usar o operador ? com seu erro em funções que retornam Result<T, Box<dyn Error>>, você precisa que seu erro implemente a trait Error.
Exemplo de definição de um enum de erro:
use std::fmt;
#[derive(Debug)]
pub enum MeuErro {
DadoInvalido(String),
NaoEncontrado { id: u32 },
ErroInterno(std::io::Error),
}
impl fmt::Display for MeuErro {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MeuErro::DadoInvalido(dados) => write!(f, "Dados inválidos: {}", dados),
MeuErro::NaoEncontrado { id } => write!(f, "Item com id {} não encontrado", id),
MeuErro::ErroInterno(erro) => write!(f, "Erro interno: {}", erro),
}
}
}Observe que derivamos Debug automaticamente e implementamos Display manualmente. A implementação de Display é importante para que a mensagem de erro seja legível. Agora, podemos usar esse erro em funções que retornam Result<T, MeuErro>.
trait Error
A trait Error está definida na biblioteca padrão (std::error::Error). Ela exige que o tipo implemente Debug e Display, e fornece métodos como source() para encadear erros. Implementar Error é simples: basta fazer impl std::error::Error for MeuErro {} (a implementação pode ser vazia se você não precisar de métodos extras).
Implementar Error permite que seu erro seja usado com Box<dyn Error> e com o operador ? em funções que retornam Result<T, Box<dyn Error>>. Além disso, bibliotecas de logging e monitoramento podem usar source() para rastrear a cadeia de erros.
Exemplo completo com implementação de Error:
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub enum MeuErro {
DadoInvalido(String),
NaoEncontrado { id: u32 },
ErroInterno(std::io::Error),
}
impl fmt::Display for MeuErro {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MeuErro::DadoInvalido(dados) => write!(f, "Dados inválidos: {}", dados),
MeuErro::NaoEncontrado { id } => write!(f, "Item com id {} não encontrado", id),
MeuErro::ErroInterno(erro) => write!(f, "Erro interno: {}", erro),
}
}
}
impl Error for MeuErro {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
MeuErro::ErroInterno(e) => Some(e),
_ => None,
}
}
}Note que sobrescrevemos o método source() para retornar o erro interno quando aplicável. Isso permite que quem lida com o erro obtenha a causa raiz.
thiserror (visão geral)
A crate thiserror simplifica drasticamente a definição de erros customizados, eliminando a necessidade de escrever manualmente as implementações de Display e Error. Basta derivar a trait thiserror::Error e usar atributos para formatar as mensagens.
Para usar, adicione thiserror = "1" ao Cargo.toml e então:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MeuErro {
#[error("Dados inválidos: {0}")]
DadoInvalido(String),
#[error("Item com id {id} não encontrado")]
NaoEncontrado { id: u32 },
#[error(transparent)]
ErroInterno(#[from] std::io::Error),
}O atributo #[error(...)] define a mensagem de Display. O #[error(transparent)] delega a exibição ao erro interno e implementa source() automaticamente. O #[from] gera uma implementação de From para converter automaticamente o erro interno no seu tipo de erro. Isso torna o código muito mais limpo e menos propenso a erros.
Além disso, thiserror implementa Error automaticamente, então você não precisa fazer isso manualmente. É uma excelente escolha para bibliotecas que precisam de erros bem definidos.
anyhow (visão geral)
Enquanto thiserror é voltado para definir erros em bibliotecas, anyhow é focado em aplicações, onde você não quer se preocupar com o tipo exato do erro. anyhow fornece um tipo anyhow::Error que pode encapsular qualquer erro que implemente Error. Ele também oferece a macro bail! para retornar erros rapidamente e o trait Context para adicionar informações contextuais.
Exemplo de uso do anyhow:
use anyhow::{Context, Result};
fn ler_arquivo(caminho: &str) -> Result<String> {
let conteudo = std::fs::read_to_string(caminho)
.with_context(|| format!("Falha ao ler o arquivo {}", caminho))?;
Ok(conteudo)
}
fn main() -> Result<()> {
let dados = ler_arquivo("config.toml")?;
println!("{}", dados);
Ok(())
}O método with_context adiciona uma mensagem descritiva ao erro original. O tipo Result<T> em anyhow é um alias para Result<T, anyhow::Error>. Isso permite que você use o operador ? com diferentes tipos de erro sem precisar convertê-los explicitamente.
anyhow é ideal para protótipos e aplicações finais, onde a flexibilidade é mais importante que a precisão do tipo de erro. Em bibliotecas, prefira thiserror para expor erros específicos.
Boas práticas e observações finais
Ao projetar erros customizados, considere:
- Use enums para categorizar erros e inclua dados relevantes em cada variante.
- Sempre implemente
DisplayeError(ou usethiserror). - Para erros que encapsulam outros erros, implemente
source()para permitir rastreamento. - Em bibliotecas, exponha seus tipos de erro publicamente; em aplicações,
anyhowsimplifica o tratamento. - Evite usar
Box<dyn Error>diretamente; prefira tipos concretos ouanyhow::Error.
Referências
- Documentação oficial da trait Error
- Tratamento de erros no Rust Book
- Crate thiserror no crates.io
- Crate anyhow no crates.io
- Clippy: dicas sobre implementação de Error
Exercícios
Crie um enum de erro chamado
ErroCalculadoracom variantesDivisaoPorZeroeRaizNegativa. ImplementeDisplaymanualmente (não use thiserror).✓ Resposta:use std::fmt; #[derive(Debug)] pub enum ErroCalculadora { DivisaoPorZero, RaizNegativa(f64), } impl fmt::Display for ErroCalculadora { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ErroCalculadora::DivisaoPorZero => write!(f, "Divisão por zero"), ErroCalculadora::RaizNegativa(valor) => write!(f, "Raiz quadrada de número negativo: {}", valor), } } } impl std::error::Error for ErroCalculadora {}Usando a crate
thiserror, reescreva o enumErroCalculadorado exercício anterior, de forma concisa.✓ Resposta:use thiserror::Error; #[derive(Error, Debug)] pub enum ErroCalculadora { #[error("Divisão por zero")] DivisaoPorZero, #[error("Raiz quadrada de número negativo: {0}")] RaizNegativa(f64), }Implemente uma função
dividir(a: f64, b: f64) -> Result<f64, ErroCalculadora>que retornaErr(ErroCalculadora::DivisaoPorZero)sebfor zero.✓ Resposta:fn dividir(a: f64, b: f64) -> Result<f64, ErroCalculadora> { if b == 0.0 { Err(ErroCalculadora::DivisaoPorZero) } else { Ok(a / b) } }Usando
anyhow, escreva uma funçãoler_numero(caminho: &str) -> anyhow::Result<f64>que lê um número de um arquivo e faz o parsing. Usewith_contextpara adicionar contexto.✓ Resposta:use anyhow::{Context, Result}; fn ler_numero(caminho: &str) -> Result<f64> { let conteudo = std::fs::read_to_string(caminho) .with_context(|| format!("Falha ao ler o arquivo {}", caminho))?; let numero: f64 = conteudo.trim().parse() .with_context(|| format!("Falha ao fazer parse do conteúdo do arquivo {}", caminho))?; Ok(numero) }Explique a diferença principal entre
thiserroreanyhowe em qual cenário cada um é mais adequado.✓ Resposta:thiserroré usado para definir tipos de erro customizados de forma concisa, focando em bibliotecas onde os erros são bem específicos e fazem parte da API pública.anyhowé usado em aplicações para lidar com erros de forma flexível, sem se preocupar com o tipo exato, permitindo adicionar contexto facilmente. Em resumo:thiserrorpara bibliotecas,anyhowpara aplicações.