Tratamento de Erros em Rust: thiserror, anyhow e Boas Práticas em 2026

Domine o tratamento de erros em Rust com thiserror, anyhow, operador ? e custom errors. Guia completo com exemplos práticos e boas práticas.

Introdução

O tratamento de erros é uma das áreas onde Rust mais se diferencia de outras linguagens. Enquanto linguagens como Java e Python usam exceções que podem surgir em qualquer ponto do código, e Go retorna tuplas (valor, error) sem verificação em tempo de compilação, Rust torna os erros parte do sistema de tipos com Result<T, E> — se uma função pode falhar, o compilador obriga você a lidar com isso.

Mas trabalhar apenas com Result e tipos de erro manuais pode ser verboso. É aí que entram crates como thiserror e anyhow, que simplificam drasticamente o tratamento de erros sem sacrificar a segurança de tipos. Neste guia, vamos desde os fundamentos do Result até padrões avançados usados em produção.

Se você ainda está aprendendo os conceitos básicos de Rust, recomendamos começar pelo nosso tutorial de tratamento de erros antes de se aprofundar neste artigo.

Result e Option — A Base de Tudo

Em Rust, o tipo Result<T, E> representa uma operação que pode ter sucesso (Ok(T)) ou falhar (Err(E)). Já o Option<T> representa um valor que pode existir (Some(T)) ou não (None). Para uma referência completa desses tipos, veja nossos guias sobre Result e Option.

use std::fs;
use std::num::ParseIntError;

fn ler_numero_do_arquivo(caminho: &str) -> Result<i32, String> {
    // Lê o conteúdo do arquivo
    let conteudo = fs::read_to_string(caminho)
        .map_err(|e| format!("Erro ao ler arquivo: {}", e))?;

    // Converte para número
    let numero: i32 = conteudo.trim().parse()
        .map_err(|e: ParseIntError| format!("Erro ao converter: {}", e))?;

    Ok(numero)
}

O operador ? propaga o erro automaticamente, mas note como precisamos usar map_err para converter entre tipos de erro diferentes. Isso funciona, mas fica verboso rapidamente.

Erros Customizados com enum

A abordagem idiomática em Rust é criar um enum que represente todos os erros possíveis do seu módulo:

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

#[derive(Debug)]
enum MeuErro {
    Io(io::Error),
    Parse(ParseIntError),
    Validacao(String),
}

impl fmt::Display for MeuErro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MeuErro::Io(e) => write!(f, "Erro de I/O: {}", e),
            MeuErro::Parse(e) => write!(f, "Erro de parsing: {}", e),
            MeuErro::Validacao(msg) => write!(f, "Validação: {}", msg),
        }
    }
}

impl std::error::Error for MeuErro {}

// Conversões automáticas com From
impl From<io::Error> for MeuErro {
    fn from(e: io::Error) -> Self {
        MeuErro::Io(e)
    }
}

impl From<ParseIntError> for MeuErro {
    fn from(e: ParseIntError) -> Self {
        MeuErro::Parse(e)
    }
}

Isso é correto e seguro, mas são mais de 30 linhas de boilerplate para três variantes de erro. Imagine um projeto real com dezenas de tipos de erro — é aqui que o thiserror brilha.

thiserror — Erros Tipados sem Boilerplate

O thiserror é uma crate de derive macro que gera automaticamente as implementações de Display, Error e From. É a escolha padrão para bibliotecas e código que precisa de erros tipados. Para entender como derive macros funcionam por baixo dos panos, confira nosso artigo sobre macros em Rust.

use thiserror::Error;

#[derive(Error, Debug)]
enum AppErro {
    #[error("Erro ao acessar arquivo: {0}")]
    Io(#[from] std::io::Error),

    #[error("Formato inválido: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("Validação falhou: {mensagem}")]
    Validacao { mensagem: String },

    #[error("Usuário '{nome}' não encontrado (id: {id})")]
    UsuarioNaoEncontrado { nome: String, id: u64 },

    #[error("Conexão com o banco falhou após {tentativas} tentativas")]
    ConexaoBanco { tentativas: u32 },
}

Com thiserror, o enum acima gera automaticamente:

  • impl Display usando as strings do atributo #[error(...)]
  • impl Error com a trait std::error::Error
  • impl From<std::io::Error> e impl From<ParseIntError> graças ao #[from]

Agora a função anterior fica muito mais limpa:

fn ler_numero_do_arquivo(caminho: &str) -> Result<i32, AppErro> {
    let conteudo = std::fs::read_to_string(caminho)?; // ? converte io::Error automaticamente
    let numero: i32 = conteudo.trim().parse()?;        // ? converte ParseIntError automaticamente

    if numero < 0 {
        return Err(AppErro::Validacao {
            mensagem: "Número deve ser positivo".into(),
        });
    }

    Ok(numero)
}

Padrões Avançados com thiserror

O thiserror suporta #[source] para encadear erros e #[backtrace] para capturar stack traces:

#[derive(Error, Debug)]
enum ServicoErro {
    #[error("Falha na requisição HTTP")]
    Http {
        #[source]
        causa: reqwest::Error,
        url: String,
    },

    #[error("Configuração inválida: {0}")]
    Config(String),

    #[error(transparent)] // Delega Display e source para o erro interno
    Outro(#[from] anyhow::Error),
}

anyhow — Erros Flexíveis para Aplicações

Enquanto o thiserror é ideal para bibliotecas com erros tipados, o anyhow é projetado para aplicações onde você quer simplicidade máxima. Ele fornece o tipo anyhow::Result<T> que aceita qualquer erro implementando std::error::Error. Para uma análise completa dessas duas crates, veja nosso guia do ecossistema anyhow e thiserror.

use anyhow::{Context, Result, bail, ensure};

fn processar_config(caminho: &str) -> Result<Config> {
    let conteudo = std::fs::read_to_string(caminho)
        .context("Falha ao ler arquivo de configuração")?;

    let config: Config = toml::from_str(&conteudo)
        .context("TOML inválido no arquivo de configuração")?;

    // bail! retorna Err imediatamente com uma mensagem
    if config.porta == 0 {
        bail!("Porta não pode ser zero");
    }

    // ensure! é como assert! mas retorna Err em vez de panic
    ensure!(config.porta < 65536, "Porta {} fora do intervalo válido", config.porta);

    Ok(config)
}

O método .context() é um dos recursos mais poderosos do anyhow — ele adiciona contexto humano aos erros, criando uma cadeia que facilita o diagnóstico:

Error: Falha ao processar pedido #42

Caused by:
    0: Falha ao ler arquivo de configuração
    1: No such file or directory (os error 2)

Quando Usar anyhow vs thiserror

CenárioRecomendação
Biblioteca pública (crate)thiserror — consumidores precisam fazer match nos erros
Aplicação bináriaanyhow — simplicidade e contexto são mais importantes
Código interno de empresaanyhow na maioria dos casos
API REST handlersthiserror para mapear erros em status HTTP
Scripts e CLIsanyhow — reportar erros é suficiente

Na prática, muitos projetos usam ambos: thiserror para definir erros da camada de domínio e anyhow na camada de aplicação.

O Operador ? em Profundidade

O operador ? é syntactic sugar que faz muito mais do que parece. Ele chama From::from() no erro para converter entre tipos, permitindo composição elegante. Para entender como o trait From funciona, veja nosso guia sobre From e Into.

use anyhow::{Context, Result};

// Composição de múltiplas operações falíveis
async fn buscar_usuario(id: u64) -> Result<Usuario> {
    let url = format!("https://api.exemplo.com/usuarios/{}", id);

    let resposta = reqwest::get(&url)
        .await
        .context("Falha na requisição HTTP")?;

    let usuario: Usuario = resposta
        .json()
        .await
        .context("Resposta não é JSON válido")?;

    // Validação com ensure!
    anyhow::ensure!(!usuario.nome.is_empty(), "Nome do usuário está vazio");

    Ok(usuario)
}

Se você está trabalhando com código assíncrono, o operador ? funciona perfeitamente com async/await — veja nosso guia completo sobre async Rust.

Padrão: Erros por Camada

Em aplicações maiores, um padrão comum é definir tipos de erro por camada usando thiserror e converter para anyhow nas bordas:

// Camada de repositório
#[derive(Error, Debug)]
enum RepoErro {
    #[error("Registro não encontrado: {0}")]
    NaoEncontrado(String),
    #[error("Violação de constraint: {0}")]
    Constraint(String),
    #[error("Erro de banco de dados")]
    Database(#[from] sqlx::Error),
}

// Camada de serviço
#[derive(Error, Debug)]
enum ServicoErro {
    #[error("Usuário não encontrado")]
    UsuarioNaoEncontrado,
    #[error("Email já cadastrado: {0}")]
    EmailDuplicado(String),
    #[error(transparent)]
    Interno(#[from] RepoErro),
}

// Camada de API — converte para respostas HTTP
impl axum::response::IntoResponse for ServicoErro {
    fn into_response(self) -> axum::response::Response {
        let (status, mensagem) = match &self {
            ServicoErro::UsuarioNaoEncontrado => {
                (StatusCode::NOT_FOUND, self.to_string())
            }
            ServicoErro::EmailDuplicado(_) => {
                (StatusCode::CONFLICT, self.to_string())
            }
            ServicoErro::Interno(_) => {
                (StatusCode::INTERNAL_SERVER_ERROR, "Erro interno".into())
            }
        };
        (status, Json(json!({ "erro": mensagem }))).into_response()
    }
}

Para aprender mais sobre como integrar esse padrão com Axum, confira nosso artigo Axum Web Framework.

Boas Práticas

  1. Nunca use .unwrap() em produção — use .expect("mensagem descritiva") ou trate o erro. Veja nosso guia sobre erros com panic e unwrap.

  2. Prefira ? sobre match quando só precisa propagar o erro.

  3. Adicione contexto com .context() ou .with_context(|| ...) para erros que cruzam fronteiras de módulo.

  4. Defina erros tipados para interfaces públicas — consumidores da sua API precisam fazer pattern matching.

  5. Use #[error(transparent)] quando uma variante de erro é apenas um wrapper.

  6. Teste seus erros — verifique que as mensagens são úteis e que os tipos corretos são retornados. Veja nosso tutorial de testes em Rust.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn teste_erro_validacao() {
        let resultado = validar_email("");
        assert!(resultado.is_err());
        let erro = resultado.unwrap_err();
        assert!(matches!(erro, AppErro::Validacao { .. }));
        assert!(erro.to_string().contains("Email"));
    }
}

Comparação com Outras Linguagens

O modelo de erros do Rust é frequentemente comparado ao de Go e ao de linguagens com exceções. Diferente de Go, onde o padrão if err != nil não é verificado pelo compilador, Rust garante em tempo de compilação que todos os erros sejam tratados. E diferente de Python, onde exceções podem surgir de qualquer lugar sem aviso, em Rust o tipo Result torna os caminhos de erro explícitos na assinatura da função.

Para comparações detalhadas entre Rust e outras linguagens, confira nossos artigos Rust vs Go e Rust vs Python.

Conclusão

O tratamento de erros em Rust é robusto por design. Com o Result<T, E> como fundação, o thiserror para erros tipados e o anyhow para aplicações, você tem ferramentas para cobrir qualquer cenário — desde bibliotecas públicas até aplicações complexas em produção, incluindo projetos de CLI e microsserviços.

A regra de ouro é: thiserror para bibliotecas, anyhow para aplicações, e contexto sempre. Com essas três diretrizes, seus erros serão informativos, tipados e fáceis de depurar.

Se você quer se aprofundar mais, confira também nosso artigo sobre boas práticas de error handling e o guia completo de Macros em Rust para entender como o derive do thiserror funciona internamente.

Veja Também