Tratamento de erros em Rust em 2026: Result, Option, thiserror e anyhow

Guia completo sobre tratamento de erros em Rust com Result, Option, o operador ?, thiserror para bibliotecas e anyhow para aplicações.

Tratamento de erros em Rust com Result, Option, thiserror e anyhow

O tratamento de erros em Rust se baseia em duas enumerações fundamentais: Result<T, E> e Option<T>. Esses tipos obrigam o desenvolvedor a tratar explicitamente os casos de sucesso, falha e ausência em tempo de compilação. Diferentemente das exceções que se propagam silenciosamente, a abordagem do Rust torna os caminhos de erro visíveis nas assinaturas de funções, eliminando categorias inteiras de surpresas em tempo de execução. O operador ?, combinado com os crates thiserror e anyhow, simplifica esse modelo explícito sem sacrificar a clareza.

Quando usar qual

Usar Option<T> para valores que podem estar legitimamente ausentes (campos de configuração, resultados de busca). Usar Result<T, E> quando operações podem falhar com informações de erro significativas (E/S de arquivos, requisições de rede, parsing).

Compreendendo os fundamentos de Result e Option

Result<T, E> representa sucesso (Ok(T)) ou falha (Err(E)). Option<T> representa um valor (Some(T)) ou ausência (None). Ambos são tipos soma — o compilador garante que cada variante seja tratada.

error_examples.rsrust
// Demonstrates Result and Option basic patterns

fn find_user(id: u64) -> Option<String> {
    // Returns None if user doesn't exist
    if id == 0 {
        None
    } else {
        Some(format!("User-{}", id))
    }
}

fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
    // Returns Err if parsing fails
    s.parse::<u16>()
}

fn main() {
    // Option handling - must address None case
    match find_user(42) {
        Some(name) => println!("Found: {}", name),
        None => println!("User not found"),
    }

    // Result handling - must address Err case
    match parse_port("8080") {
        Ok(port) => println!("Port: {}", port),
        Err(e) => println!("Invalid port: {}", e),
    }
}

O compilador rejeita código que ignora esses valores de retorno sem tratamento explícito. Esse design detecta bugs em tempo de compilação que se manifestariam como exceções de ponteiro nulo ou erros não capturados em outras linguagens.

O operador de interrogação para propagação concisa

O operador ? transforma cadeias verbosas de match em código linear legível. Quando aplicado a um Result, retorna antecipadamente com o erro se presente, ou extrai o valor de sucesso. O mesmo se aplica a Option.

file_reader.rsrust
// Using ? for clean error propagation

use std::fs::File;
use std::io::{self, BufRead, BufReader};

fn read_first_line(path: &str) -> Result<String, io::Error> {
    let file = File::open(path)?;  // Returns early if open fails
    let mut reader = BufReader::new(file);
    let mut line = String::new();
    reader.read_line(&mut line)?;  // Returns early if read fails
    Ok(line.trim().to_string())
}

fn get_port_from_config(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
    let content = read_first_line(path)?;
    let port = content.parse::<u16>()?;  // ParseIntError converts via From
    Ok(port)
}

O operador ? exige que o tipo de erro seja conversível para o tipo de erro de retorno da função através do trait From. Usar Box<dyn std::error::Error> como mostrado acima aceita qualquer tipo de erro que implemente o trait Error padrão.

Criando erros personalizados com thiserror

O crate thiserror elimina o código repetitivo para tipos de erro personalizados. Ele deriva as implementações de Error, Display e From através de uma macro procedural.

errors.rsrust
// Custom error types using thiserror 2.0

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("configuration file not found at {path}")]
    NotFound { path: String },

    #[error("invalid port number: {0}")]
    InvalidPort(#[from] std::num::ParseIntError),

    #[error("IO error reading config")]
    IoError(#[from] std::io::Error),

    #[error("missing required field: {0}")]
    MissingField(String),
}

fn load_config(path: &str) -> Result<Config, ConfigError> {
    let content = std::fs::read_to_string(path)?;  // IoError auto-converts
    let port: u16 = content
        .lines()
        .find(|l| l.starts_with("port="))
        .ok_or(ConfigError::MissingField("port".into()))?
        .strip_prefix("port=")
        .unwrap()
        .parse()?;  // ParseIntError auto-converts to InvalidPort

    Ok(Config { port })
}

struct Config {
    port: u16,
}

O atributo #[from] gera conversões automáticas, permitindo o uso fluido de ? com diferentes tipos de erro subjacentes. As mensagens de erro se tornam autodocumentadas através das strings de formato #[error(...)].

Erros no nível da aplicação com anyhow

Enquanto thiserror é adequado para código de biblioteca com tipos de erro específicos, anyhow é voltado para aplicações onde o contexto do erro importa mais do que a granularidade de tipos. Seu trait Context adiciona mensagens descritivas a qualquer erro.

main.rsrust
// Application error handling with anyhow 1.0

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

fn load_database_url() -> Result<String> {
    std::env::var("DATABASE_URL")
        .context("DATABASE_URL environment variable not set")
}

fn connect_to_database(url: &str) -> Result<DatabaseConnection> {
    ensure!(!url.is_empty(), "database URL cannot be empty");

    let conn = DatabaseConnection::new(url)
        .context("failed to establish database connection")?;

    if !conn.is_healthy() {
        bail!("database connection unhealthy after establishment");
    }

    Ok(conn)
}

fn main() -> Result<()> {
    let url = load_database_url()?;
    let conn = connect_to_database(&url)
        .context("application startup failed")?;

    // Context chains create readable error traces:
    // Error: application startup failed
    // Caused by:
    //     0: failed to establish database connection
    //     1: connection refused

    Ok(())
}

struct DatabaseConnection;
impl DatabaseConnection {
    fn new(_url: &str) -> Result<Self> { Ok(Self) }
    fn is_healthy(&self) -> bool { true }
}

O método context() encapsula erros com informações adicionais, criando uma cadeia que facilita a depuração. A macro bail! fornece uma saída antecipada com uma mensagem de erro formatada, enquanto ensure! age como uma asserção que retorna um erro em vez de entrar em pânico.

Pronto para mandar bem nas entrevistas de Rust?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Combinando thiserror e anyhow em projetos reais

Bibliotecas expõem erros estruturados via thiserror para tratamento programático pelos consumidores. Aplicações encapsulam esses erros com anyhow para saída legível por humanos. Essa separação mantém as APIs limpas enquanto preserva a capacidade de depuração.

lib.rs - Library code with thiserrorrust
// Exposes typed errors for programmatic handling

use thiserror::Error;

#[derive(Error, Debug)]
pub enum PaymentError {
    #[error("insufficient funds: required {required}, available {available}")]
    InsufficientFunds { required: u64, available: u64 },

    #[error("card declined: {reason}")]
    CardDeclined { reason: String },

    #[error("payment provider unavailable")]
    ProviderUnavailable(#[source] reqwest::Error),
}

pub fn process_payment(amount: u64) -> Result<Receipt, PaymentError> {
    // Library returns specific, matchable error types
    Err(PaymentError::InsufficientFunds {
        required: amount,
        available: 50,
    })
}

pub struct Receipt;
main.rs - Application code with anyhowrust
// Wraps library errors with context

use anyhow::{Context, Result};
use my_payment_lib::{process_payment, PaymentError};

fn checkout(cart_total: u64) -> Result<()> {
    match process_payment(cart_total) {
        Ok(_receipt) => Ok(()),
        Err(PaymentError::InsufficientFunds { required, available }) => {
            // Handle specific case differently
            println!("Add {} to your balance", required - available);
            Ok(())
        }
        Err(e) => Err(e).context("checkout payment processing failed"),
    }
}

Esse padrão permite que os chamadores façam match em variantes específicas quando a recuperação é possível, enquanto se beneficiam do contexto de erro enriquecido ao propagar falhas para cima. A comunidade Rust padronizou amplamente essa abordagem, como discutido nas diretrizes de API do Rust.

Padrões de tratamento de erros para código assíncrono

Funções assíncronas retornam Result assim como as síncronas. O operador ? funciona de maneira idêntica dentro de blocos async, e tanto thiserror quanto anyhow se integram sem modificação.

async_errors.rsrust
// Error handling in async Rust with Tokio

use anyhow::{Context, Result};
use std::time::Duration;

async fn fetch_user_data(user_id: u64) -> Result<UserData> {
    let response = reqwest::get(format!("https://api.example.com/users/{}", user_id))
        .await
        .context("HTTP request to user API failed")?;

    let status = response.status();
    if !status.is_success() {
        anyhow::bail!("user API returned status {}", status);
    }

    let data: UserData = response
        .json()
        .await
        .context("failed to parse user data JSON")?;

    Ok(data)
}

async fn fetch_with_retry(user_id: u64, attempts: u32) -> Result<UserData> {
    let mut last_error = None;

    for attempt in 1..=attempts {
        match fetch_user_data(user_id).await {
            Ok(data) => return Ok(data),
            Err(e) => {
                last_error = Some(e);
                if attempt < attempts {
                    tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await;
                }
            }
        }
    }

    Err(last_error.unwrap()).context(format!("failed after {} attempts", attempts))
}

#[derive(serde::Deserialize)]
struct UserData {
    name: String,
}

Ao combinar múltiplas operações assíncronas, usar try_join! de tokio ou futures para executá-las concorrentemente enquanto propaga o primeiro erro.

Downcasting e inspeção de erros

Tanto anyhow::Error quanto Box<dyn Error> suportam downcasting para recuperar o tipo de erro original. Isso permite registrar detalhes específicos enquanto ainda se propagam erros genéricos.

downcasting.rsrust
// Inspecting wrapped error types

use anyhow::{Context, Result};
use std::io;

fn log_and_propagate(result: Result<()>) -> Result<()> {
    if let Err(ref e) = result {
        // Check if the root cause is a specific type
        if let Some(io_err) = e.downcast_ref::<io::Error>() {
            match io_err.kind() {
                io::ErrorKind::NotFound => {
                    tracing::warn!("file not found, using defaults");
                }
                io::ErrorKind::PermissionDenied => {
                    tracing::error!("permission denied - check file ownership");
                }
                _ => {
                    tracing::error!("IO error: {:?}", io_err);
                }
            }
        }
    }
    result
}

O downcasting faz a ponte entre o tratamento de erros genérico e a lógica de recuperação específica. Usá-lo com moderação — se o downcasting ocorre frequentemente, considerar se uma enumeração de erros tipados serviria melhor.

Conversão entre Option e Result

A biblioteca padrão fornece métodos para converter entre Option e Result, permitindo uma composição fluida quando diferentes APIs usam diferentes padrões.

conversions.rsrust
// Option and Result interoperability

fn get_env_port() -> Option<u16> {
    std::env::var("PORT")
        .ok()  // Result -> Option (discards error)
        .and_then(|s| s.parse().ok())
}

fn get_env_port_with_error() -> Result<u16, String> {
    std::env::var("PORT")
        .map_err(|_| "PORT not set".to_string())?
        .parse()
        .map_err(|_| "PORT is not a valid number".to_string())
}

fn lookup_and_parse(map: &std::collections::HashMap<String, String>, key: &str) -> Result<u16, String> {
    map.get(key)
        .ok_or_else(|| format!("key '{}' not found", key))?  // Option -> Result
        .parse()
        .map_err(|e| format!("parse error for '{}': {}", key, e))
}

O método ok() descarta os detalhes de erro quando apenas a presença importa. Os métodos ok_or() e ok_or_else() convertem None em um erro personalizado, permitindo a propagação com ? a partir de valores Option.

Considerações de desempenho

O tratamento de erros em Rust não tem custo em tempo de execução no caminho de sucesso. Result e Option são enumerações alocadas na pilha com tamanho determinístico. O compilador otimiza as verificações quando pode provar que um branch é inalcançável.

| Abordagem | Custo caminho sucesso | Custo caminho falha | |-----------|----------------------|---------------------| | Result/Option | Zero | Desenrolamento de pilha (barato) | | panic! | Zero | Desenrolamento completo de pilha + limpeza | | Exceções C++ | Zero (geralmente) | Alocação heap cara + RTTI |

Evitar unwrap() e expect() em código de biblioteca — reservá-los para casos onde a falha genuinamente indica um bug. Para caminhos críticos em desempenho onde erros são comuns, considerar usar enumerações com dados inline em vez de tipos de erro alocados no heap.

Conclusão

  • Result<T, E> trata falhas recuperáveis; Option<T> trata ausência — ambos impõem tratamento em tempo de compilação
  • O operador ? propaga erros de maneira concisa, exigindo implementações do trait From para conversão de tipos
  • thiserror gera tipos de erro estruturados para bibliotecas sem boilerplate
  • anyhow fornece cadeias de erros contextuais para aplicações, suportando context(), bail! e ensure!
  • Combinar ambos: bibliotecas expõem erros tipados via thiserror, aplicações os encapsulam com anyhow
  • Código assíncrono usa padrões idênticos — ? funciona em blocos async sem modificação
  • Usar ok_or() para converter Option em Result; usar ok() para o inverso
  • Fazer downcast de erros encapsulados apenas quando a lógica de recuperação específica requer o tipo original

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

#rust
#tratamento-erros
#result
#option
#thiserror
#anyhow

Compartilhar

Artigos relacionados