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:
std::fmt::Debug— representação para desenvolvedoresstd::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
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.
Sempre implemente
source(): Quando seu erro encapsula outro, retorne-o emsource(). Isso permite rastrear a causa raiz.Display para o usuário, Debug para o desenvolvedor: A mensagem de
Displaydeve ser clara e orientada ao problema.Debug(via derive) mostra a estrutura interna.Use
Frompara conversão automática: ImplementeFrom<SubErro> for MeuErropara que o operador?converta erros automaticamente. Veja From e Into.Considere
thiserrorpara bibliotecas: O cratethiserrorelimina boilerplate sem custo em runtime. Para aplicações,anyhowoferece ergonomia adicional.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.Não exponha erros internos: Se você está criando uma biblioteca, não exponha
io::Errorouserde_json::Errordiretamente na sua API pública. Encapsule-os no seu tipo de erro.
Veja Também
- Display e Debug — pré-requisitos do trait Error
- From e Into — conversão de erros com operador
? - Tratamento de Erros — tutorial completo sobre erros em Rust
- Boas Práticas de Error Handling — padrões avançados de tratamento de erros