Criando tipos de erro customizados
1. Por que criar tipos de erro customizados?
Em Rust, o tratamento de erros é uma parte fundamental da linguagem. Embora seja possível usar String ou &str como tipos de erro em Result<T, E>, essa abordagem tem limitações significativas:
- Falta de estrutura: Strings não permitem distinguir diferentes categorias de erro
- Perda de informação: Não é possível anexar dados contextuais como códigos de erro ou causas originais
- Dificuldade de manutenção: Erros espalhados como strings literais são difíceis de rastrear e modificar
- Sem integração com traits: A trait
std::error::Errornão pode ser implementada para tipos primitivos comoString
Tipos de erro customizados oferecem clareza, segurança de tipo e documentação embutida, além de integração perfeita com o ecossistema de tratamento de erros do Rust.
2. Estruturando um tipo de erro simples
A maneira mais comum de criar um tipo de erro customizado é usando um enum que representa diferentes variantes de erro:
use std::fmt;
#[derive(Debug)]
enum ErroProcessamento {
ArquivoNaoEncontrado,
FormatoInvalido(String),
PermissaoNegada,
ErroDesconhecido,
}
impl fmt::Display for ErroProcessamento {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErroProcessamento::ArquivoNaoEncontrado => {
write!(f, "Arquivo não encontrado")
}
ErroProcessamento::FormatoInvalido(detalhes) => {
write!(f, "Formato inválido: {}", detalhes)
}
ErroProcessamento::PermissaoNegada => {
write!(f, "Permissão negada para acessar o recurso")
}
ErroProcessamento::ErroDesconhecido => {
write!(f, "Erro desconhecido durante o processamento")
}
}
}
}
3. Implementando a trait std::error::Error
A trait Error exige que o tipo implemente Debug e Display. Ela fornece métodos opcionais como source() para referenciar a causa do erro:
use std::error::Error;
use std::fmt;
#[derive(Debug)]
enum ErroProcessamento {
ArquivoNaoEncontrado,
FormatoInvalido(String),
PermissaoNegada,
ErroDesconhecido,
}
impl fmt::Display for ErroProcessamento {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErroProcessamento::ArquivoNaoEncontrado => {
write!(f, "Arquivo não encontrado")
}
ErroProcessamento::FormatoInvalido(detalhes) => {
write!(f, "Formato inválido: {}", detalhes)
}
ErroProcessamento::PermissaoNegada => {
write!(f, "Permissão negada")
}
ErroProcessamento::ErroDesconhecido => {
write!(f, "Erro desconhecido")
}
}
}
}
impl Error for ErroProcessamento {}
4. Encadeamento de erros com a causa original
Para preservar a cadeia de erros, podemos usar o método source() e armazenar a causa original dentro do tipo customizado:
use std::error::Error;
use std::fmt;
use std::io;
#[derive(Debug)]
enum ErroProcessamento {
ArquivoNaoEncontrado,
FormatoInvalido(String),
ErroIo(io::Error),
ErroComCausa {
mensagem: String,
causa: Box<dyn Error + Send + Sync>,
},
}
impl fmt::Display for ErroProcessamento {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErroProcessamento::ArquivoNaoEncontrado => {
write!(f, "Arquivo não encontrado")
}
ErroProcessamento::FormatoInvalido(detalhes) => {
write!(f, "Formato inválido: {}", detalhes)
}
ErroProcessamento::ErroIo(err) => {
write!(f, "Erro de I/O: {}", err)
}
ErroProcessamento::ErroComCausa { mensagem, .. } => {
write!(f, "{}", mensagem)
}
}
}
}
impl Error for ErroProcessamento {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ErroProcessamento::ErroIo(err) => Some(err),
ErroProcessamento::ErroComCausa { causa, .. } => Some(causa.as_ref()),
_ => None,
}
}
}
5. Criando erros com campos adicionais
Estruturas com campos nomeados permitem adicionar metadados úteis como códigos de erro, timestamps e mensagens contextuais:
use std::error::Error;
use std::fmt;
use std::time::SystemTime;
#[derive(Debug)]
struct ErroApi {
status_code: u16,
mensagem: String,
timestamp: SystemTime,
causa: Option<Box<dyn Error + Send + Sync>>,
}
impl fmt::Display for ErroApi {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Erro na API (status {}): {}",
self.status_code, self.mensagem
)
}
}
impl Error for ErroApi {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.causa.as_ref().map(|c| c.as_ref())
}
}
impl ErroApi {
fn new(status_code: u16, mensagem: String) -> Self {
ErroApi {
status_code,
mensagem,
timestamp: SystemTime::now(),
causa: None,
}
}
fn com_causa(status_code: u16, mensagem: String, causa: Box<dyn Error + Send + Sync>) -> Self {
ErroApi {
status_code,
mensagem,
timestamp: SystemTime::now(),
causa: Some(causa),
}
}
}
6. Conversão de erros com From e Into
Implementar From<T> permite converter automaticamente erros externos no seu tipo customizado, facilitando o uso do operador ?:
use std::error::Error;
use std::fmt;
use std::io;
#[derive(Debug)]
enum ErroAplicacao {
ErroIo(io::Error),
ErroParse(String),
ErroGenerico(String),
}
impl fmt::Display for ErroAplicacao {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErroAplicacao::ErroIo(err) => write!(f, "Erro de I/O: {}", err),
ErroAplicacao::ErroParse(msg) => write!(f, "Erro de parse: {}", msg),
ErroAplicacao::ErroGenerico(msg) => write!(f, "Erro: {}", msg),
}
}
}
impl Error for ErroAplicacao {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ErroAplicacao::ErroIo(err) => Some(err),
_ => None,
}
}
}
impl From<io::Error> for ErroAplicacao {
fn from(err: io::Error) -> Self {
ErroAplicacao::ErroIo(err)
}
}
impl From<String> for ErroAplicacao {
fn from(msg: String) -> Self {
ErroAplicacao::ErroGenerico(msg)
}
}
// Exemplo de uso com o operador ?
fn ler_arquivo(caminho: &str) -> Result<String, ErroAplicacao> {
let conteudo = std::fs::read_to_string(caminho)?; // Converte io::Error automaticamente
Ok(conteudo)
}
7. Boas práticas e padrões avançados
Para reduzir boilerplate, a crate thiserror simplifica a definição de tipos de erro customizados:
use thiserror::Error;
#[derive(Error, Debug)]
enum ErroAplicacao {
#[error("Arquivo não encontrado: {0}")]
ArquivoNaoEncontrado(String),
#[error("Erro de I/O: {0}")]
ErroIo(#[from] io::Error),
#[error("Erro de parse: {0}")]
ErroParse(#[from] serde_json::Error),
#[error("Erro desconhecido: {0}")]
ErroDesconhecido(String),
}
Considerações importantes:
- Erros recuperáveis vs. irrecuperáveis: Use
Result<T, E>para erros que podem ser tratados epanic!para situações que indicam bugs - Send + Sync: Em ambientes concorrentes, garanta que seus tipos de erro implementem
SendeSyncse forem usados em threads - Documentação: Use comentários e documentação para descrever cada variante de erro e quando ela ocorre
Exemplo final completo:
use std::error::Error;
use std::fmt;
use std::io;
#[derive(Debug)]
struct ErroServidor {
codigo: u32,
mensagem: String,
causa: Option<Box<dyn Error + Send + Sync>>,
}
impl fmt::Display for ErroServidor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[Erro {}] {}", self.codigo, self.mensagem)
}
}
impl Error for ErroServidor {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.causa.as_ref().map(|c| c.as_ref())
}
}
impl From<io::Error> for ErroServidor {
fn from(err: io::Error) -> Self {
ErroServidor {
codigo: 1001,
mensagem: format!("Erro de I/O: {}", err),
causa: Some(Box::new(err)),
}
}
}
fn processar_arquivo(nome: &str) -> Result<(), ErroServidor> {
let _conteudo = std::fs::read_to_string(nome)?;
Ok(())
}
Referências
- The Rust Programming Language - Error Handling — Capítulo oficial sobre tratamento de erros em Rust, incluindo tipos customizados
- Rust by Example - Error Handling — Exemplos práticos de criação e uso de tipos de erro
- std::error::Error - Rust Documentation — Documentação oficial da trait Error e seus métodos
- thiserror crate documentation — Documentação da crate thiserror para definição simplificada de erros
- Rust Design Patterns - Error Handling — Padrões de design para tratamento de erros em Rust
- Error Handling in Rust - A Comprehensive Guide — Guia detalhado sobre tratamento de erros por Andrew Gallant (BurntSushi)
- Rust API Guidelines - Error Types — Diretrizes oficiais para design de tipos de erro em APIs Rust