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::Error não pode ser implementada para tipos primitivos como String

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 e panic! para situações que indicam bugs
  • Send + Sync: Em ambientes concorrentes, garanta que seus tipos de erro implementem Send e Sync se 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