Tratamento de Erros em Rust: Result e Option | Rust Brasil

Domine Result, Option e o operador ? em Rust. Tutorial completo com thiserror, anyhow e erros personalizados em português.

Rust adota uma abordagem única para tratamento de erros: em vez de exceções (como Java/Python) ou códigos de retorno (como C), Rust usa tipos algébricosResult<T, E> e Option<T> — que forçam o programador a lidar com erros explicitamente. Isso resulta em código mais robusto e confiável.

Dois Tipos de Erros em Rust

Rust distingue entre dois tipos de erros:

  1. Erros recuperáveis — Situações esperadas que o programa pode tratar (arquivo não encontrado, input inválido). Representados por Result<T, E>.
  2. Erros irrecuperáveis — Bugs que indicam um estado inválido do programa (acesso fora dos limites de um array). Causam panic!.

panic! — Erros Irrecuperáveis

Quando algo dá muito errado e não há como continuar:

fn main() {
    // panic! explícito
    // panic!("Algo deu muito errado!");

    // panic! implícito (acesso fora dos limites)
    let vetor = vec![1, 2, 3];
    // let valor = vetor[99]; // panic: index out of bounds

    println!("Este código roda normalmente.");
    println!("Use panic! apenas para bugs, não para erros esperados.");
}

Na prática, você raramente usa panic! diretamente. Use Result para erros que podem acontecer normalmente.

Option — Valores que Podem Não Existir

Option<T> representa um valor que pode ou não estar presente. É a alternativa segura ao null de outras linguagens:

// Definição na biblioteca padrão:
// enum Option<T> {
//     Some(T),   // contém um valor
//     None,      // não contém valor
// }

fn encontrar_primeiro_par(numeros: &[i32]) -> Option<i32> {
    for &n in numeros {
        if n % 2 == 0 {
            return Some(n);
        }
    }
    None
}

fn dividir_seguro(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None
    } else {
        Some(a / b)
    }
}

fn main() {
    // Usando match
    let numeros = vec![1, 3, 5, 8, 9];
    match encontrar_primeiro_par(&numeros) {
        Some(n) => println!("Primeiro par: {}", n),
        None => println!("Nenhum número par encontrado"),
    }

    // Usando if let
    if let Some(resultado) = dividir_seguro(10.0, 3.0) {
        println!("10 / 3 = {:.2}", resultado);
    }

    if let Some(resultado) = dividir_seguro(10.0, 0.0) {
        println!("10 / 0 = {:.2}", resultado);
    } else {
        println!("Divisão por zero!");
    }
}

Métodos Úteis de Option

fn main() {
    let algum_valor: Option<i32> = Some(42);
    let nenhum_valor: Option<i32> = None;

    // unwrap_or: valor padrão se None
    println!("{}", algum_valor.unwrap_or(0));   // 42
    println!("{}", nenhum_valor.unwrap_or(0));  // 0

    // unwrap_or_else: closure para calcular valor padrão
    let resultado = nenhum_valor.unwrap_or_else(|| {
        println!("Calculando valor padrão...");
        -1
    });
    println!("Resultado: {}", resultado);

    // map: transforma o valor interno
    let texto: Option<String> = Some(String::from("olá"));
    let maiusculo: Option<String> = texto.map(|s| s.to_uppercase());
    println!("{:?}", maiusculo); // Some("OLÁ")

    // and_then (flatmap): encadeia operações que retornam Option
    let numero: Option<&str> = Some("42");
    let parsed: Option<i32> = numero.and_then(|s| s.parse().ok());
    println!("{:?}", parsed); // Some(42)

    // is_some e is_none
    println!("algum_valor existe? {}", algum_valor.is_some()); // true
    println!("nenhum_valor existe? {}", nenhum_valor.is_some()); // false

    // filter: mantém Some apenas se a condição for verdadeira
    let valor = Some(10);
    let par = valor.filter(|&x| x % 2 == 0);
    let impar = valor.filter(|&x| x % 2 != 0);
    println!("Par: {:?}, Ímpar: {:?}", par, impar); // Some(10), None
}

Result<T, E> — Operações que Podem Falhar

Result<T, E> representa o resultado de uma operação que pode suceder (Ok(T)) ou falhar (Err(E)):

// Definição na biblioteca padrão:
// enum Result<T, E> {
//     Ok(T),    // sucesso com valor T
//     Err(E),   // erro com valor E
// }

use std::num::ParseIntError;

fn parse_idade(texto: &str) -> Result<u32, String> {
    match texto.trim().parse::<u32>() {
        Ok(idade) if idade <= 150 => Ok(idade),
        Ok(idade) => Err(format!("Idade {} é inválida (máximo 150)", idade)),
        Err(e) => Err(format!("Não foi possível converter '{}': {}", texto, e)),
    }
}

fn main() {
    let entradas = vec!["25", "abc", "200", " 30 ", "-5"];

    for entrada in entradas {
        match parse_idade(entrada) {
            Ok(idade) => println!("'{}' -> Idade válida: {} anos", entrada, idade),
            Err(erro) => println!("'{}' -> Erro: {}", entrada, erro),
        }
    }
}

Saída:

'25' -> Idade válida: 25 anos
'abc' -> Erro: Não foi possível converter 'abc': invalid digit found in string
'200' -> Erro: Idade 200 é inválida (máximo 150)
' 30 ' -> Idade válida: 30 anos
'-5' -> Erro: Não foi possível converter '-5': invalid digit found in string

unwrap e expect

Para prototipagem rápida, unwrap e expect extraem o valor de um Result ou Option, mas causam panic se houver erro:

fn main() {
    // unwrap: panic com mensagem genérica se for Err/None
    let numero: i32 = "42".parse().unwrap();
    println!("Número: {}", numero);

    // expect: panic com SUA mensagem se for Err/None
    let numero: i32 = "42".parse().expect("Falha ao converter número");
    println!("Número: {}", numero);

    // PERIGOSO — causaria panic:
    // let _erro: i32 = "abc".parse().unwrap();
    // let _erro: i32 = "abc".parse().expect("Não é um número válido");

    // unwrap é aceitável quando você TEM CERTEZA que não vai falhar
    let lista = vec![1, 2, 3];
    let primeiro = lista.first().unwrap(); // sabemos que a lista não está vazia
    println!("Primeiro: {}", primeiro);
}

Regra geral: Use unwrap/expect apenas em protótipos, testes, ou quando você tem certeza absoluta de que o valor existe. Em código de produção, trate os erros adequadamente.

O Operador ? — Propagação Elegante de Erros

O operador ? é o recurso mais elegante do Rust para tratamento de erros. Ele propaga o erro automaticamente para quem chamou a função:

use std::fs;
use std::io;

fn ler_nome_do_arquivo(caminho: &str) -> Result<String, io::Error> {
    let conteudo = fs::read_to_string(caminho)?; // se Err, retorna o erro
    Ok(conteudo.trim().to_string())
}

// Sem o operador ?, seria assim:
fn ler_nome_do_arquivo_verbose(caminho: &str) -> Result<String, io::Error> {
    let conteudo = match fs::read_to_string(caminho) {
        Ok(c) => c,
        Err(e) => return Err(e),
    };
    Ok(conteudo.trim().to_string())
}

fn main() {
    match ler_nome_do_arquivo("/tmp/teste.txt") {
        Ok(conteudo) => println!("Conteúdo: {}", conteudo),
        Err(e) => println!("Erro ao ler arquivo: {}", e),
    }
}

Encadeando o Operador ?

O verdadeiro poder do ? aparece quando você encadeia várias operações que podem falhar:

use std::fs;
use std::io;
use std::path::Path;

fn contar_linhas_arquivo(caminho: &str) -> Result<usize, io::Error> {
    let conteudo = fs::read_to_string(caminho)?;
    let linhas = conteudo.lines().count();
    Ok(linhas)
}

fn processar_arquivo(caminho: &str) -> Result<String, io::Error> {
    let conteudo = fs::read_to_string(caminho)?;
    let linhas: Vec<&str> = conteudo.lines().collect();
    let total = linhas.len();

    let resumo = format!(
        "Arquivo: {}\nLinhas: {}\nPrimeira linha: {}",
        caminho,
        total,
        linhas.first().unwrap_or(&"(vazio)")
    );

    Ok(resumo)
}

fn main() {
    // Exemplo com arquivo que pode não existir
    match processar_arquivo("dados.txt") {
        Ok(resumo) => println!("{}", resumo),
        Err(e) => println!("Não foi possível processar: {}", e),
    }
}

O Operador ? com Option

O ? também funciona com Option, retornando None se o valor não existir:

fn obter_extensao(nome_arquivo: &str) -> Option<&str> {
    let ponto_pos = nome_arquivo.rfind('.')?;  // retorna None se não encontrar
    Some(&nome_arquivo[ponto_pos + 1..])
}

fn obter_nome_sem_extensao(nome_arquivo: &str) -> Option<&str> {
    let ponto_pos = nome_arquivo.rfind('.')?;
    Some(&nome_arquivo[..ponto_pos])
}

fn main() {
    let arquivos = vec!["foto.jpg", "documento.pdf", "README", "codigo.rs"];

    for arquivo in arquivos {
        match obter_extensao(arquivo) {
            Some(ext) => println!("{} -> extensão: {}", arquivo, ext),
            None => println!("{} -> sem extensão", arquivo),
        }
    }
}

Tipos de Erro Personalizados

Para aplicações reais, você vai querer criar seus próprios tipos de erro:

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

#[derive(Debug)]
enum AppErro {
    ArquivoNaoEncontrado(String),
    FormatoInvalido(String),
    ParseErro(ParseIntError),
    SemPermissao,
}

// Implementar Display para mensagens amigáveis
impl fmt::Display for AppErro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppErro::ArquivoNaoEncontrado(caminho) => {
                write!(f, "Arquivo não encontrado: {}", caminho)
            }
            AppErro::FormatoInvalido(msg) => {
                write!(f, "Formato inválido: {}", msg)
            }
            AppErro::ParseErro(e) => {
                write!(f, "Erro ao converter valor: {}", e)
            }
            AppErro::SemPermissao => {
                write!(f, "Sem permissão para executar esta operação")
            }
        }
    }
}

// Converter ParseIntError em AppErro automaticamente
impl From<ParseIntError> for AppErro {
    fn from(e: ParseIntError) -> Self {
        AppErro::ParseErro(e)
    }
}

fn processar_linha(linha: &str) -> Result<i32, AppErro> {
    if linha.is_empty() {
        return Err(AppErro::FormatoInvalido(
            "Linha vazia".to_string()
        ));
    }

    // O ? converte ParseIntError em AppErro automaticamente (via From)
    let numero: i32 = linha.trim().parse()?;
    Ok(numero * 2)
}

fn main() {
    let linhas = vec!["42", "", "abc", "7"];

    for linha in linhas {
        match processar_linha(linha) {
            Ok(resultado) => println!("'{}' -> {}", linha, resultado),
            Err(e) => println!("'{}' -> Erro: {}", linha, e),
        }
    }
}

Saída:

'42' -> 84
'' -> Erro: Formato inválido: Linha vazia
'abc' -> Erro: Erro ao converter valor: invalid digit found in string
'7' -> 14

thiserror: Erros Personalizados Simplificados

A crate thiserror elimina o boilerplate de criar tipos de erro. Adicione ao Cargo.toml:

[dependencies]
thiserror = "2"

Agora compare a versão manual com a versão usando thiserror:

use thiserror::Error;

#[derive(Debug, Error)]
enum AppErro {
    #[error("Arquivo não encontrado: {0}")]
    ArquivoNaoEncontrado(String),

    #[error("Formato inválido: {0}")]
    FormatoInvalido(String),

    #[error("Erro ao converter valor")]
    ParseErro(#[from] std::num::ParseIntError),

    #[error("Sem permissão para executar esta operação")]
    SemPermissao,

    #[error("Erro de IO: {0}")]
    IoErro(#[from] std::io::Error),
}

fn processar_arquivo(caminho: &str) -> Result<Vec<i32>, AppErro> {
    let conteudo = std::fs::read_to_string(caminho)
        .map_err(|_| AppErro::ArquivoNaoEncontrado(caminho.to_string()))?;

    let mut numeros = Vec::new();
    for linha in conteudo.lines() {
        if !linha.is_empty() {
            let n: i32 = linha.trim().parse()?;  // converte automaticamente
            numeros.push(n);
        }
    }

    Ok(numeros)
}

fn main() {
    match processar_arquivo("numeros.txt") {
        Ok(nums) => println!("Números: {:?}", nums),
        Err(e) => println!("Erro: {}", e),
    }
}

O thiserror gera automaticamente as implementações de Display, Error e From. Muito menos código para o mesmo resultado!

anyhow: Tratamento de Erros Simplificado para Aplicações

Enquanto thiserror é ideal para bibliotecas (onde você quer tipos de erro bem definidos), a crate anyhow é perfeita para aplicações (onde você quer simplicidade).

Adicione ao Cargo.toml:

[dependencies]
anyhow = "1"
use anyhow::{Context, Result, bail, ensure};

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

    ensure!(!conteudo.is_empty(), "Arquivo de configuração está vazio");

    Ok(conteudo)
}

fn parse_porta(texto: &str) -> Result<u16> {
    let porta: u16 = texto.parse()
        .context(format!("'{}' não é uma porta válida", texto))?;

    if porta < 1024 {
        bail!("Porta {} requer privilégios de administrador", porta);
    }

    Ok(porta)
}

fn iniciar_app() -> Result<()> {
    let _config = ler_configuracao("config.toml")
        .context("Falha ao inicializar aplicação")?;

    let _porta = parse_porta("8080")
        .context("Falha ao configurar porta do servidor")?;

    println!("Aplicação iniciada com sucesso!");
    Ok(())
}

fn main() {
    if let Err(e) = iniciar_app() {
        // anyhow mostra a cadeia completa de erros
        eprintln!("Erro: {}", e);

        // Mostrar a cadeia de contextos
        for causa in e.chain().skip(1) {
            eprintln!("  Causado por: {}", causa);
        }
    }
}

Funcionalidades do anyhow:

  • Result<T> — Alias para Result<T, anyhow::Error>, aceita qualquer tipo de erro
  • .context() — Adiciona contexto a um erro (muito útil para debugging)
  • bail! — Retorna um erro imediatamente (como um return Err(...) mais conciso)
  • ensure! — Verifica uma condição e retorna erro se falsa

thiserror vs. anyhow: Quando Usar Cada Um

CenárioUsePor quê
Biblioteca (crate pública)thiserrorUsuários precisam de tipos de erro bem definidos para tratá-los
Aplicação (binário)anyhowSimplicidade; erros geralmente são reportados ao usuário
Código interno complexoAmbosthiserror para domínio, anyhow na camada de aplicação

Padrões Comuns no Dia a Dia

Convertendo entre Option e Result

fn main() {
    // Option -> Result
    let valor: Option<i32> = Some(42);
    let resultado: Result<i32, &str> = valor.ok_or("Valor não encontrado");
    println!("{:?}", resultado); // Ok(42)

    let nada: Option<i32> = None;
    let resultado: Result<i32, &str> = nada.ok_or("Valor não encontrado");
    println!("{:?}", resultado); // Err("Valor não encontrado")

    // Result -> Option
    let ok: Result<i32, &str> = Ok(42);
    let opcao: Option<i32> = ok.ok();
    println!("{:?}", opcao); // Some(42)
}

Coletando Results de um Iterator

fn main() {
    let textos = vec!["1", "2", "3", "4", "5"];

    // Coletar todos ou falhar no primeiro erro
    let numeros: Result<Vec<i32>, _> = textos
        .iter()
        .map(|t| t.parse::<i32>())
        .collect();

    println!("Todos válidos: {:?}", numeros); // Ok([1, 2, 3, 4, 5])

    let textos_mistos = vec!["1", "dois", "3"];
    let resultado: Result<Vec<i32>, _> = textos_mistos
        .iter()
        .map(|t| t.parse::<i32>())
        .collect();

    println!("Com erro: {:?}", resultado); // Err(ParseIntError)

    // Separar sucessos de erros
    let (sucessos, erros): (Vec<_>, Vec<_>) = textos_mistos
        .iter()
        .map(|t| t.parse::<i32>())
        .partition(Result::is_ok);

    let sucessos: Vec<i32> = sucessos.into_iter().map(Result::unwrap).collect();
    let erros: Vec<_> = erros.into_iter().map(Result::unwrap_err).collect();

    println!("Sucessos: {:?}", sucessos);  // [1, 3]
    println!("Erros: {:?}", erros);        // [invalid digit found in string]
}

Exemplo Prático: Validador de Dados

use std::collections::HashMap;

#[derive(Debug)]
struct ValidacaoErro {
    campo: String,
    mensagem: String,
}

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

fn validar_nome(nome: &str) -> Result<(), ValidacaoErro> {
    if nome.trim().is_empty() {
        return Err(ValidacaoErro {
            campo: "nome".to_string(),
            mensagem: "não pode estar vazio".to_string(),
        });
    }
    if nome.len() < 3 {
        return Err(ValidacaoErro {
            campo: "nome".to_string(),
            mensagem: "deve ter pelo menos 3 caracteres".to_string(),
        });
    }
    Ok(())
}

fn validar_email(email: &str) -> Result<(), ValidacaoErro> {
    if !email.contains('@') {
        return Err(ValidacaoErro {
            campo: "email".to_string(),
            mensagem: "deve conter @".to_string(),
        });
    }
    if !email.contains('.') {
        return Err(ValidacaoErro {
            campo: "email".to_string(),
            mensagem: "deve conter um domínio válido".to_string(),
        });
    }
    Ok(())
}

fn validar_idade(idade: &str) -> Result<u32, ValidacaoErro> {
    let idade: u32 = idade.parse().map_err(|_| ValidacaoErro {
        campo: "idade".to_string(),
        mensagem: format!("'{}' não é um número válido", idade),
    })?;

    if idade < 18 {
        return Err(ValidacaoErro {
            campo: "idade".to_string(),
            mensagem: "deve ser maior de 18 anos".to_string(),
        });
    }

    Ok(idade)
}

fn validar_cadastro(dados: &HashMap<&str, &str>) -> Result<(), Vec<ValidacaoErro>> {
    let mut erros = Vec::new();

    if let Some(nome) = dados.get("nome") {
        if let Err(e) = validar_nome(nome) {
            erros.push(e);
        }
    } else {
        erros.push(ValidacaoErro {
            campo: "nome".to_string(),
            mensagem: "campo obrigatório".to_string(),
        });
    }

    if let Some(email) = dados.get("email") {
        if let Err(e) = validar_email(email) {
            erros.push(e);
        }
    } else {
        erros.push(ValidacaoErro {
            campo: "email".to_string(),
            mensagem: "campo obrigatório".to_string(),
        });
    }

    if let Some(idade) = dados.get("idade") {
        if let Err(e) = validar_idade(idade) {
            erros.push(e);
        }
    }

    if erros.is_empty() {
        Ok(())
    } else {
        Err(erros)
    }
}

fn main() {
    println!("=== Cadastro Válido ===");
    let mut dados = HashMap::new();
    dados.insert("nome", "Maria Silva");
    dados.insert("email", "maria@exemplo.com");
    dados.insert("idade", "25");

    match validar_cadastro(&dados) {
        Ok(()) => println!("Cadastro válido!"),
        Err(erros) => {
            println!("Erros encontrados:");
            for e in &erros {
                println!("  - {}", e);
            }
        }
    }

    println!("\n=== Cadastro Inválido ===");
    let mut dados_invalidos = HashMap::new();
    dados_invalidos.insert("nome", "Al");
    dados_invalidos.insert("email", "invalido");
    dados_invalidos.insert("idade", "15");

    match validar_cadastro(&dados_invalidos) {
        Ok(()) => println!("Cadastro válido!"),
        Err(erros) => {
            println!("Erros encontrados:");
            for e in &erros {
                println!("  - {}", e);
            }
        }
    }
}

Saída:

=== Cadastro Válido ===
Cadastro válido!

=== Cadastro Inválido ===
Erros encontrados:
  - Campo 'nome': deve ter pelo menos 3 caracteres
  - Campo 'email': deve conter @
  - Campo 'idade': deve ser maior de 18 anos

Boas Práticas para Tratamento de Erros

  1. Nunca use unwrap() em código de produção — a menos que tenha 100% de certeza de que o valor existe
  2. Prefira ? em vez de match para propagação — é mais legível
  3. Use context() do anyhow — facilita muito o debugging em produção
  4. Crie tipos de erro específicos para bibliotecas com thiserror
  5. Use anyhow para aplicações — menos boilerplate, mais produtividade
  6. Erros devem ser informativos — inclua contexto sobre o que aconteceu e por quê
  7. Documente quais erros uma função pode retornar — ajuda quem usa seu código

Veja Também

Conclusão

O sistema de tratamento de erros do Rust pode parecer verboso no início, mas ele te força a pensar sobre o que pode dar errado — e isso resulta em software muito mais robusto. Com Result, Option, o operador ? e crates como thiserror e anyhow, você tem todas as ferramentas para escrever código que lida com erros de forma elegante e segura.

Parabéns por completar esta série de tutoriais introdutórios! Agora você tem uma base sólida em Rust. Continue praticando, explore o Rust Book e participe da comunidade Rust Brasil para trocar experiências com outros Rustáceos!