---
title: "Anyhow vs Thiserror: Error Handling em Rust | Rust Brasil"
url: "https://rustlang.com.br/artigos/boas-praticas-error-handling/"
markdown_url: "https://rustlang.com.br/artigos/boas-praticas-error-handling.MD"
description: "Boas práticas de error handling em Rust: anyhow vs thiserror, tipos customizados, propagação com ? e como evitar unwrap() em produção."
date: "2026-02-23"
author: "Equipe Rust Brasil"
---

# Anyhow vs Thiserror: Error Handling em Rust | Rust Brasil

Boas práticas de error handling em Rust: anyhow vs thiserror, tipos customizados, propagação com ? e como evitar unwrap() em produção.


## Introdução

O tratamento de erros é uma das áreas onde Rust mais se diferencia de outras linguagens. Enquanto linguagens como Python e Java usam exceções e Go retorna tuplas `(valor, erro)`, Rust adota um sistema baseado em tipos com `Result<T, E>` e `Option<T>` que **força** o programador a lidar com cada possível falha em tempo de compilação.

Essa abordagem elimina toda uma classe de bugs — erros não tratados simplesmente não compilam. Porém, escrever código robusto de tratamento de erros vai além de usar `Result`. Neste artigo, vamos explorar as boas práticas que profissionais experientes aplicam em projetos Rust em produção, desde a criação de tipos de erro customizados até o uso estratégico de crates como `thiserror` e `anyhow`.

## O Problema: Código com Tratamento de Erros Frágil

Vamos começar com exemplos de **como NÃO tratar erros** em Rust. Estes padrões são comuns em código de iniciantes e podem causar problemas sérios em produção.

### Não Faça Isso: `unwrap()` em Todo Lugar

```rust
use std::fs;
use std::collections::HashMap;

fn carregar_configuracao() -> HashMap<String, String> {
    // ERRADO: Se o arquivo não existir, o programa entra em panic
    let conteudo = fs::read_to_string("config.toml").unwrap();

    // ERRADO: Se o parse falhar, panic novamente
    let config: HashMap<String, String> = toml::from_str(&conteudo).unwrap();

    config
}

fn main() {
    let config = carregar_configuracao();
    // ERRADO: Se a chave não existir, panic
    let porta = config.get("porta").unwrap();
    println!("Servidor na porta: {porta}");
}
```

Cada `.unwrap()` é uma bomba-relógio. Se **qualquer** dessas operações falhar, o programa inteiro entra em panic com uma mensagem genérica como `called 'Option::unwrap()' on a 'None' value`, sem contexto sobre o que deu errado.

### Não Faça Isso: Strings como Tipo de Erro

```rust
fn dividir(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        // ERRADO: String como tipo de erro dificulta pattern matching
        return Err("Divisão por zero não é permitida".to_string());
    }
    Ok(a / b)
}

fn calcular() -> Result<f64, String> {
    let x = dividir(10.0, 0.0)?;
    // Erros de diferentes origens viram uma sopa de strings
    let arquivo = std::fs::read_to_string("dados.txt")
        .map_err(|e| e.to_string())?;
    Ok(x)
}
```

Usar `String` como tipo de erro parece simples, mas remove a capacidade de distinguir entre tipos de erros e torna impossível para o código chamador tratar erros específicos de forma diferente.

## A Solução: Tipos de Erro Customizados e Propagação Elegante

### Faça Isso: Tipos de Erro com `thiserror` (para Bibliotecas)

Para bibliotecas, use a crate `thiserror` para criar tipos de erro expressivos e ergonômicos:

```toml
# Cargo.toml
[dependencies]
thiserror = "2"
```

```rust
use std::io;
use std::num::ParseIntError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("Arquivo de configuração não encontrado: {caminho}")]
    ArquivoNaoEncontrado {
        caminho: String,
        #[source]
        fonte: io::Error,
    },

    #[error("Formato inválido na linha {linha}: {mensagem}")]
    FormatoInvalido {
        linha: usize,
        mensagem: String,
    },

    #[error("Valor inválido para a porta: {0}")]
    PortaInvalida(#[from] ParseIntError),

    #[error("Chave obrigatória ausente: {0}")]
    ChaveAusente(String),
}

pub fn carregar_configuracao(caminho: &str) -> Result<Config, ConfigError> {
    let conteudo = std::fs::read_to_string(caminho)
        .map_err(|e| ConfigError::ArquivoNaoEncontrado {
            caminho: caminho.to_string(),
            fonte: e,
        })?;

    let porta_str = extrair_valor(&conteudo, "porta")
        .ok_or_else(|| ConfigError::ChaveAusente("porta".to_string()))?;

    let porta: u16 = porta_str.parse()?; // Usa From<ParseIntError> automaticamente

    Ok(Config { porta })
}

pub struct Config {
    pub porta: u16,
}

fn extrair_valor<'a>(conteudo: &'a str, chave: &str) -> Option<&'a str> {
    for linha in conteudo.lines() {
        if let Some(valor) = linha.strip_prefix(&format!("{chave} = ")) {
            return Some(valor.trim());
        }
    }
    None
}
```

Com `thiserror`, cada variante de erro tem uma mensagem descritiva, pode encapsular a causa raiz (`#[source]`), e suporta conversão automática (`#[from]`).

### Faça Isso: `anyhow` para Aplicações

Para aplicações (binários), use `anyhow` que fornece um tipo de erro genérico com suporte a contexto:

```toml
# Cargo.toml
[dependencies]
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```

```rust
use anyhow::{Context, Result, bail};
use serde::Deserialize;

#[derive(Deserialize)]
struct Config {
    porta: u16,
    host: String,
    banco_de_dados: String,
}

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

    let config: Config = serde_json::from_str(&conteudo)
        .with_context(|| format!("Falha ao fazer parse do JSON em: {caminho}"))?;

    if config.porta == 0 {
        bail!("A porta não pode ser zero no arquivo {caminho}");
    }

    Ok(config)
}

fn main() -> Result<()> {
    let config = carregar_configuracao("config.json")
        .context("Não foi possível inicializar o servidor")?;

    println!("Servidor iniciando em {}:{}", config.host, config.porta);
    Ok(())
}
```

A grande vantagem do `anyhow` é o método `.context()` que adiciona camadas de informação ao erro. Quando o erro é exibido, você vê toda a cadeia causal:

```
Error: Não foi possível inicializar o servidor

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

## Guia Passo a Passo: Implementando Error Handling Robusto

### Passo 1: Defina a Estratégia — Biblioteca vs Aplicação

A regra de ouro é:

| Tipo de projeto | Crate recomendada | Tipo de erro |
|---|---|---|
| **Biblioteca** | `thiserror` | Enum customizada |
| **Aplicação** | `anyhow` | `anyhow::Result` |
| **Misto** | Ambas | `thiserror` na API pública, `anyhow` internamente |

### Passo 2: Use o Operador `?` para Propagação

O operador `?` é a forma idiomática de propagar erros em Rust. Ele extrai o valor de um `Result::Ok` ou retorna o erro automaticamente:

```rust
use anyhow::{Context, Result};

fn ler_numero_do_arquivo(caminho: &str) -> Result<i32> {
    let conteudo = std::fs::read_to_string(caminho)
        .with_context(|| format!("Não foi possível ler {caminho}"))?;

    let numero: i32 = conteudo.trim().parse()
        .with_context(|| format!("Conteúdo de {caminho} não é um número válido"))?;

    Ok(numero)
}
```

### Passo 3: Use `expect()` Apenas com Invariantes Lógicos

A diferença entre `unwrap()` e `expect()` é que `expect()` exige uma mensagem descritiva. Mesmo assim, use-o apenas quando a falha representa um bug lógico no programa:

```rust
use std::sync::Mutex;

fn main() {
    let dados = Mutex::new(vec![1, 2, 3]);

    // OK: Se o Mutex está envenenado, é um bug no programa
    let guard = dados.lock()
        .expect("Mutex envenenado — outro thread entrou em panic");

    // ERRADO: Entrada do usuário pode falhar legitimamente
    // let porta: u16 = input.parse().expect("Porta inválida");
}
```

### Passo 4: Implemente `Display` e `Error` Manualmente (Quando Necessário)

Sem `thiserror`, você pode implementar os traits manualmente para controle total:

```rust
use std::fmt;
use std::error::Error;

#[derive(Debug)]
pub enum AppError {
    Database(String),
    Validation(String),
    NotFound { recurso: String, id: u64 },
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Database(msg) => write!(f, "Erro de banco de dados: {msg}"),
            AppError::Validation(msg) => write!(f, "Erro de validação: {msg}"),
            AppError::NotFound { recurso, id } => {
                write!(f, "{recurso} com id {id} não encontrado")
            }
        }
    }
}

impl Error for AppError {}
```

### Passo 5: Converta Erros com `From`

Implemente `From` para converter entre tipos de erros automaticamente (o operador `?` usa essa conversão):

```rust
use std::io;
use std::num::ParseIntError;

#[derive(Debug)]
pub enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
}

impl From<io::Error> for AppError {
    fn from(err: io::Error) -> Self {
        AppError::Io(err)
    }
}

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

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "Erro de I/O: {e}"),
            AppError::Parse(e) => write!(f, "Erro de parse: {e}"),
        }
    }
}

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

fn processar_arquivo(caminho: &str) -> Result<i32, AppError> {
    let conteudo = std::fs::read_to_string(caminho)?; // io::Error → AppError
    let numero: i32 = conteudo.trim().parse()?;        // ParseIntError → AppError
    Ok(numero * 2)
}
```

## Armadilhas Comuns e Como Evitá-las

### 1. Usar `unwrap()` em Código Assíncrono

```rust
// ERRADO: panic em uma task Tokio pode ser silencioso
async fn buscar_dados(url: &str) -> String {
    let resp = reqwest::get(url).await.unwrap(); // Panic silencioso!
    resp.text().await.unwrap()
}

// CORRETO: Propague o erro
async fn buscar_dados(url: &str) -> Result<String, reqwest::Error> {
    let resp = reqwest::get(url).await?;
    let texto = resp.text().await?;
    Ok(texto)
}
```

### 2. Ignorar Erros com `let _ =`

```rust
// ERRADO: Você está ignorando um possível erro
let _ = std::fs::remove_file("temp.txt");

// MELHOR: Log se falhar, mas continue
if let Err(e) = std::fs::remove_file("temp.txt") {
    eprintln!("Aviso: não foi possível remover temp.txt: {e}");
}
```

### 3. `map_err` Sem Contexto Suficiente

```rust
// ERRADO: Perde informação sobre o erro original
let conteudo = std::fs::read_to_string("config.toml")
    .map_err(|_| "Erro ao ler arquivo")?;

// CORRETO com anyhow: Preserva o erro original e adiciona contexto
use anyhow::Context;
let conteudo = std::fs::read_to_string("config.toml")
    .context("Erro ao ler config.toml")?;
```

### 4. Panic em Construtores

```rust
// ERRADO: new() que pode falhar não deveria usar panic
impl Servidor {
    pub fn new(porta: u16) -> Self {
        let listener = std::net::TcpListener::bind(format!("0.0.0.0:{porta}"))
            .unwrap(); // panic se a porta estiver em uso!
        Servidor { listener }
    }
}

// CORRETO: Retorne Result para operações que podem falhar
impl Servidor {
    pub fn new(porta: u16) -> Result<Self, std::io::Error> {
        let listener = std::net::TcpListener::bind(format!("0.0.0.0:{porta}"))?;
        Ok(Servidor { listener })
    }
}

struct Servidor {
    listener: std::net::TcpListener,
}
```

## Exemplos do Mundo Real

### API REST com Axum e Tratamento de Erros

```toml
# Cargo.toml
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
```

```rust
use axum::{
    extract::Path,
    http::StatusCode,
    response::{IntoResponse, Response},
    routing::get,
    Json, Router,
};
use serde::Serialize;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ApiError {
    #[error("Usuário não encontrado: {0}")]
    NotFound(u64),

    #[error("Erro interno: {0}")]
    Internal(String),
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, mensagem) = match &self {
            ApiError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
            ApiError::Internal(_) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Erro interno do servidor".to_string(),
            ),
        };

        let body = serde_json::json!({ "erro": mensagem });
        (status, Json(body)).into_response()
    }
}

#[derive(Serialize)]
struct Usuario {
    id: u64,
    nome: String,
}

async fn buscar_usuario(Path(id): Path<u64>) -> Result<Json<Usuario>, ApiError> {
    if id == 0 {
        return Err(ApiError::NotFound(id));
    }

    // Simula busca no banco de dados
    Ok(Json(Usuario {
        id,
        nome: "Maria Silva".to_string(),
    }))
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/usuarios/{id}", get(buscar_usuario));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}
```

### CLI com Tratamento de Erros usando `anyhow`

```toml
# Cargo.toml
[dependencies]
anyhow = "1"
```

```rust
use anyhow::{Context, Result, bail};
use std::env;
use std::fs;

fn main() -> Result<()> {
    let args: Vec<String> = env::args().collect();

    let caminho = args.get(1)
        .context("Uso: programa <caminho-do-arquivo>")?;

    let conteudo = fs::read_to_string(caminho)
        .with_context(|| format!("Não foi possível ler o arquivo '{caminho}'"))?;

    let total_linhas = conteudo.lines().count();

    if total_linhas == 0 {
        bail!("O arquivo '{caminho}' está vazio");
    }

    println!("O arquivo '{caminho}' tem {total_linhas} linhas.");
    Ok(())
}
```

Quando `main()` retorna `Result<()>`, erros não tratados são exibidos automaticamente no stderr com toda a cadeia de contexto.

## Resumo das Boas Práticas

| Prática | Quando Usar |
|---|---|
| `thiserror` | Bibliotecas — erros tipados e expressivos |
| `anyhow` | Aplicações — erros com contexto empilhado |
| Operador `?` | Sempre — para propagação limpa |
| `.context()` / `.with_context()` | Sempre — adicione contexto a cada ponto de falha |
| `expect("motivo")` | Apenas para invariantes lógicos |
| `unwrap()` | Apenas em testes e protótipos |
| Pattern matching em erros | Quando você precisa reagir diferente a cada tipo de erro |

---

## Veja Também

- [Tutorial: Tratamento de Erros em Rust](/tutoriais/tratamento-de-erros/) — Guia completo de Result, Option e o operador ?
- [Receita: Tratar Erros em Rust](/receitas/tratar-erros/) — Exemplos práticos de tratamento de erros
- [Erros Comuns: panic e unwrap](/erros/panic-unwrap/) — Como resolver panics causados por unwrap
- [Padrões de Projeto em Rust](/artigos/padroes-projeto-rust/) — Builder, Newtype, Typestate e outros padrões
- [Segurança em Rust](/artigos/seguranca-rust/) — Práticas de segurança além da memória
- [Logging e Observabilidade em Rust](/artigos/logging-observabilidade/) — Como monitorar erros em produção

---

Veja como outras linguagens abordam o tratamento de erros:

- <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Error handling em Go: a filosofia de erros explícitos com if err != nil</a>
- <a href="https://kotlin.dev.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'kotlin.dev.br' })">Tratamento de exceções em Kotlin: try-catch, sealed classes e Result</a>
