std::error::Error Trait em Rust

Guia completo sobre o trait Error em Rust: tipos de erro customizados, cadeia source(), Display, downcasting e integração com thiserror.

O que é o Trait Error?

O trait std::error::Error é a base do sistema de tratamento de erros em Rust. Ele define a interface que todo tipo de erro deve seguir, permitindo que erros sejam compostos, encadeados e inspecionados de forma uniforme.

Qualquer tipo que implementa Error pode:

  • Ser usado como Box<dyn Error> para erros genéricos
  • Encadear erros com source() para rastrear a causa raiz
  • Ser formatado para o usuário via Display
  • Ser formatado para depuração via Debug
  • Ser convertido via downcasting para o tipo concreto

O trait exige que o tipo implemente tanto Debug quanto Display, garantindo que todo erro tem uma representação textual.


Definição do Trait

// std::error::Error
pub trait Error: Debug + Display {
    // Retorna a causa/fonte deste erro (se houver)
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        None
    }
}

O trait tem uma implementação padrão para source() que retorna None. Você sobrescreve quando seu erro encapsula outro erro.

Requisitos

Para implementar Error, seu tipo deve implementar:

  1. std::fmt::Debug — representação para desenvolvedores
  2. std::fmt::Display — mensagem legível para o usuário

Como Implementar

Implementação manual completa

use std::fmt;
use std::error::Error;

#[derive(Debug)]
enum AppErro {
    NaoEncontrado { recurso: String },
    SemPermissao { acao: String },
    Interno(String),
}

impl fmt::Display for AppErro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppErro::NaoEncontrado { recurso } => {
                write!(f, "Recurso não encontrado: {}", recurso)
            }
            AppErro::SemPermissao { acao } => {
                write!(f, "Sem permissão para: {}", acao)
            }
            AppErro::Interno(msg) => {
                write!(f, "Erro interno: {}", msg)
            }
        }
    }
}

impl Error for AppErro {}  // source() retorna None por padrão

fn buscar_usuario(id: u32) -> Result<String, AppErro> {
    if id == 0 {
        Err(AppErro::NaoEncontrado {
            recurso: format!("Usuário #{}", id),
        })
    } else {
        Ok(format!("Usuário #{}", id))
    }
}

fn main() {
    match buscar_usuario(0) {
        Ok(u) => println!("Encontrado: {}", u),
        Err(e) => {
            eprintln!("Erro: {}", e);
            eprintln!("Debug: {:?}", e);
        }
    }
}

Implementação com cadeia de erros (source)

use std::fmt;
use std::error::Error;
use std::num::ParseIntError;
use std::io;

#[derive(Debug)]
enum ConfigErro {
    ArquivoNaoEncontrado(io::Error),
    ValorInvalido {
        campo: String,
        causa: ParseIntError,
    },
    CampoAusente(String),
}

impl fmt::Display for ConfigErro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigErro::ArquivoNaoEncontrado(_) => {
                write!(f, "Arquivo de configuração não encontrado")
            }
            ConfigErro::ValorInvalido { campo, .. } => {
                write!(f, "Valor inválido para o campo '{}'", campo)
            }
            ConfigErro::CampoAusente(campo) => {
                write!(f, "Campo obrigatório ausente: '{}'", campo)
            }
        }
    }
}

impl Error for ConfigErro {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ConfigErro::ArquivoNaoEncontrado(e) => Some(e),
            ConfigErro::ValorInvalido { causa, .. } => Some(causa),
            ConfigErro::CampoAusente(_) => None,
        }
    }
}

// Conversões com From para usar com ?
impl From<io::Error> for ConfigErro {
    fn from(e: io::Error) -> Self {
        ConfigErro::ArquivoNaoEncontrado(e)
    }
}

fn carregar_config(caminho: &str) -> Result<u32, ConfigErro> {
    let conteudo = std::fs::read_to_string(caminho)?;

    let porta: u32 = conteudo.trim().parse().map_err(|e| {
        ConfigErro::ValorInvalido {
            campo: String::from("porta"),
            causa: e,
        }
    })?;

    Ok(porta)
}

fn imprimir_cadeia_erros(erro: &dyn Error) {
    eprintln!("Erro: {}", erro);
    let mut fonte = erro.source();
    while let Some(e) = fonte {
        eprintln!("  Causado por: {}", e);
        fonte = e.source();
    }
}

fn main() {
    match carregar_config("config.txt") {
        Ok(porta) => println!("Porta: {}", porta),
        Err(e) => imprimir_cadeia_erros(&e),
    }
    // Saída possível:
    // Erro: Arquivo de configuração não encontrado
    //   Causado por: No such file or directory (os error 2)
}

Exemplos Práticos

Exemplo 1: Box para erros genéricos

use std::error::Error;
use std::fs;

// Box<dyn Error> aceita qualquer tipo que implemente Error
fn processar(caminho: &str) -> Result<i32, Box<dyn Error>> {
    let conteudo = fs::read_to_string(caminho)?;  // io::Error
    let numero: i32 = conteudo.trim().parse()?;    // ParseIntError
    Ok(numero * 2)
}

fn main() {
    match processar("dados.txt") {
        Ok(resultado) => println!("Resultado: {}", resultado),
        Err(e) => {
            eprintln!("Erro: {}", e);
            // Percorrer a cadeia de erros
            let mut fonte = e.source();
            while let Some(causa) = fonte {
                eprintln!("  Causado por: {}", causa);
                fonte = causa.source();
            }
        }
    }
}

Exemplo 2: Downcasting de erros

use std::error::Error;
use std::io;

fn operacao() -> Result<(), Box<dyn Error>> {
    let _arquivo = std::fs::File::open("inexistente.txt")?;
    Ok(())
}

fn main() {
    if let Err(erro) = operacao() {
        // Tenta fazer downcast para io::Error
        if let Some(io_erro) = erro.downcast_ref::<io::Error>() {
            match io_erro.kind() {
                io::ErrorKind::NotFound => {
                    eprintln!("Arquivo não encontrado!");
                }
                io::ErrorKind::PermissionDenied => {
                    eprintln!("Sem permissão!");
                }
                _ => {
                    eprintln!("Erro de I/O: {}", io_erro);
                }
            }
        } else {
            eprintln!("Erro desconhecido: {}", erro);
        }
    }
}

Exemplo 3: Tipo de erro com contexto

use std::error::Error;
use std::fmt;
use std::io;

#[derive(Debug)]
struct ErroComContexto {
    mensagem: String,
    fonte: Box<dyn Error + 'static>,
}

impl ErroComContexto {
    fn novo(mensagem: impl Into<String>, fonte: impl Error + 'static) -> Self {
        ErroComContexto {
            mensagem: mensagem.into(),
            fonte: Box::new(fonte),
        }
    }
}

impl fmt::Display for ErroComContexto {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.mensagem)
    }
}

impl Error for ErroComContexto {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&*self.fonte)
    }
}

// Trait de extensão para adicionar contexto a qualquer Result
trait ResultExt<T> {
    fn contexto(self, msg: impl Into<String>) -> Result<T, ErroComContexto>;
}

impl<T, E: Error + 'static> ResultExt<T> for Result<T, E> {
    fn contexto(self, msg: impl Into<String>) -> Result<T, ErroComContexto> {
        self.map_err(|e| ErroComContexto::novo(msg, e))
    }
}

fn ler_config() -> Result<String, ErroComContexto> {
    std::fs::read_to_string("/etc/app/config.toml")
        .contexto("Falha ao ler arquivo de configuração")
}

fn main() {
    match ler_config() {
        Ok(config) => println!("Config: {}", config),
        Err(e) => {
            eprintln!("Erro: {}", e);
            if let Some(fonte) = e.source() {
                eprintln!("  Causa: {}", fonte);
            }
        }
    }
}

Exemplo 4: Integração com thiserror

O crate thiserror automatiza a implementação de Error, Display e From:

// No Cargo.toml: thiserror = "2"

// use thiserror::Error;
//
// #[derive(Debug, Error)]
// enum ServicoErro {
//     #[error("Usuário não encontrado: {id}")]
//     UsuarioNaoEncontrado { id: u64 },
//
//     #[error("Falha na conexão com o banco")]
//     Banco(#[from] io::Error),
//
//     #[error("Token expirado")]
//     TokenExpirado,
//
//     #[error("Valor inválido: {0}")]
//     Validacao(String),
//
//     #[error("Erro ao processar dados")]
//     Processamento {
//         #[source]
//         causa: serde_json::Error,
//     },
// }

// O código acima é equivalente a implementar manualmente:
use std::error::Error;
use std::fmt;
use std::io;

#[derive(Debug)]
enum ServicoErro {
    UsuarioNaoEncontrado { id: u64 },
    Banco(io::Error),
    TokenExpirado,
    Validacao(String),
}

impl fmt::Display for ServicoErro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ServicoErro::UsuarioNaoEncontrado { id } => {
                write!(f, "Usuário não encontrado: {}", id)
            }
            ServicoErro::Banco(_) => write!(f, "Falha na conexão com o banco"),
            ServicoErro::TokenExpirado => write!(f, "Token expirado"),
            ServicoErro::Validacao(msg) => write!(f, "Valor inválido: {}", msg),
        }
    }
}

impl Error for ServicoErro {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ServicoErro::Banco(e) => Some(e),
            _ => None,
        }
    }
}

impl From<io::Error> for ServicoErro {
    fn from(e: io::Error) -> Self {
        ServicoErro::Banco(e)
    }
}

fn main() {
    let erro = ServicoErro::UsuarioNaoEncontrado { id: 42 };
    eprintln!("{}", erro); // Usuário não encontrado: 42
}

Exemplo 5: Percorrendo a cadeia completa de erros

use std::error::Error;

fn imprimir_cadeia_completa(erro: &dyn Error) {
    println!("Erro: {}", erro);

    let mut nivel = 1;
    let mut atual = erro.source();

    while let Some(causa) = atual {
        println!("{}Causa (nível {}): {}", "  ".repeat(nivel), nivel, causa);
        atual = causa.source();
        nivel += 1;
    }
}

fn coletar_mensagens(erro: &dyn Error) -> Vec<String> {
    let mut mensagens = vec![erro.to_string()];
    let mut atual = erro.source();
    while let Some(causa) = atual {
        mensagens.push(causa.to_string());
        atual = causa.source();
    }
    mensagens
}

fn main() {
    // Simulando um erro encadeado
    let io_err = std::io::Error::new(
        std::io::ErrorKind::ConnectionRefused,
        "conexão recusada pelo servidor"
    );

    let mensagens = coletar_mensagens(&io_err);
    for (i, msg) in mensagens.iter().enumerate() {
        println!("[{}] {}", i, msg);
    }
}

Padrões e Boas Práticas

  1. Crie enums de erro por módulo/crate: Cada módulo deveria ter seu próprio enum de erro que cobre todos os casos de falha possíveis.

  2. Sempre implemente source(): Quando seu erro encapsula outro, retorne-o em source(). Isso permite rastrear a causa raiz.

  3. Display para o usuário, Debug para o desenvolvedor: A mensagem de Display deve ser clara e orientada ao problema. Debug (via derive) mostra a estrutura interna.

  4. Use From para conversão automática: Implemente From<SubErro> for MeuErro para que o operador ? converta erros automaticamente. Veja From e Into.

  5. Considere thiserror para bibliotecas: O crate thiserror elimina boilerplate sem custo em runtime. Para aplicações, anyhow oferece ergonomia adicional.

  6. Box<dyn Error> para protótipos: Em código exploratório ou exemplos, Result<T, Box<dyn Error>> é prático. Para produção, prefira tipos de erro concretos.

  7. Não exponha erros internos: Se você está criando uma biblioteca, não exponha io::Error ou serde_json::Error diretamente na sua API pública. Encapsule-os no seu tipo de erro.


Veja Também