Rust

Tratamento de Erros com Result — Erros como Valores, não Exceções Já leu

11 min de leitura

Tratamento de Erros com Result — Erros como Valores, não Exceções
  Em linguagens como Java, Python e C#, erros são tratados com exceções — um mecanismo que interrompe o fluxo normal do program

 

Em linguagens como Java, Python e C#, erros são tratados com exceções — um mecanismo que interrompe o fluxo normal do programa e "joga" o erro para cima na pilha de chamadas até alguém capturá-lo com try/catch. É conveniente, mas tem um custo: qualquer função pode falhar de forma invisível, e o compilador não te obriga a tratar os erros. Você descobre que esqueceu de tratar um caso apenas quando o programa quebra em produção.

Rust faz diferente. Erros são valores — retornados explicitamente, tratados explicitamente, e verificados pelo compilador. Nenhum erro pode ser silenciosamente ignorado.


Dois tipos de erro em Rust

Rust distingue duas categorias de falha:

Erros irrecuperáveis — situações onde o programa não tem como continuar: acesso a índice fora dos limites, falha de alocação de memória, violação de invariante crítica. Para esses casos, Rust usa panic!, que encerra o programa imediatamente com uma mensagem de erro.

Erros recuperáveis — situações esperadas que o programa pode tratar: arquivo não encontrado, entrada inválida do usuário, falha de rede. Para esses casos, Rust usa Result<T, E>.

A maioria dos erros que você vai lidar no dia a dia são recuperáveis. É aqui que Result brilha.


O enum Result<T, E>

Result é definido na biblioteca padrão como:

enum Result<T, E> {
    Ok(T),   // sucesso: contém o valor do tipo T
    Err(E),  // falha: contém o erro do tipo E
}

T e E são parâmetros genéricos — T é o tipo do valor em caso de sucesso, e E é o tipo do erro em caso de falha. Assim como Option, Result é tão fundamental que Ok e Err estão disponíveis sem qualificação.

Um exemplo direto — uma função que pode falhar:

fn dividir(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Divisão por zero não é permitida"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    match dividir(10.0, 2.0) {
        Ok(resultado) => println!("Resultado: {resultado}"),
        Err(e)        => println!("Erro: {e}"),
    }

    match dividir(10.0, 0.0) {
        Ok(resultado) => println!("Resultado: {resultado}"),
        Err(e)        => println!("Erro: {e}"),
    }
}

Saída:

Resultado: 5
Erro: Divisão por zero não é permitida

O compilador exige que você trate ambos os casos. Não há como usar o valor dentro de Ok sem verificar se é realmente um Ok.


Lendo um arquivo — erro do mundo real

Vamos ver um exemplo mais realista — ler o conteúdo de um arquivo:

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

fn ler_arquivo(caminho: &str) -> Result<String, io::Error> {
    fs::read_to_string(caminho)
}

fn main() {
    match ler_arquivo("config.txt") {
        Ok(conteudo) => {
            println!("Arquivo lido com sucesso:");
            println!("{conteudo}");
        }
        Err(e) => {
            println!("Falha ao ler arquivo: {e}");
        }
    }
}

fs::read_to_string já retorna Result<String, io::Error> — simplesmente propagamos esse resultado. Se o arquivo não existir, você receberá um Err descritivo em vez de um crash.


O operador ? — propagação elegante de erros

Imagine que você tem várias operações que podem falhar em sequência. Tratar cada uma com match ficaria verboso:

fn processar() -> Result<String, io::Error> {
    let conteudo = match fs::read_to_string("entrada.txt") {
        Ok(c)  => c,
        Err(e) => return Err(e),
    };

    // mais operações que podem falhar...
    Ok(conteudo)
}

O operador ? faz exatamente isso — mas em uma única linha:

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

fn processar() -> Result<String, io::Error> {
    let conteudo = fs::read_to_string("entrada.txt")?;
    let resultado = format!("Processado: {}", conteudo.trim());
    Ok(resultado)
}

fn main() {
    match processar() {
        Ok(r)  => println!("{r}"),
        Err(e) => println!("Erro: {e}"),
    }
}

O ? no final de uma expressão Result faz o seguinte: se o resultado for Ok(valor), desempacota e retorna valor. Se for Err(e), retorna imediatamente da função com Err(e). É propagação automática de erros — sem boilerplate.

Uma restrição importante: ? só pode ser usado em funções que retornam Result ou Option. O compilador verifica isso.


Encadeando operações com ?

O poder real do ? aparece quando você encadeia múltiplas operações falíveis:

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

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

impl From<io::Error> for MeuErro {
    fn from(e: io::Error) -> MeuErro {
        MeuErro::Io(e)
    }
}

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

fn somar_arquivo(caminho: &str) -> Result<i64, MeuErro> {
    let conteudo = fs::read_to_string(caminho)?; // io::Error → MeuErro
    let mut soma = 0i64;

    for linha in conteudo.lines() {
        let numero: i64 = linha.trim().parse()?; // ParseIntError → MeuErro
        soma += numero;
    }

    Ok(soma)
}

fn main() {
    match somar_arquivo("numeros.txt") {
        Ok(soma)                  => println!("Soma: {soma}"),
        Err(MeuErro::Io(e))      => println!("Erro de IO: {e}"),
        Err(MeuErro::Parse(e))   => println!("Erro de parse: {e}"),
    }
}

O trait From permite que o ? converta automaticamente entre tipos de erro. Quando você usa ? numa operação que retorna io::Error, e sua função retorna MeuErro, o compilador chama MeuErro::from(e) automaticamente. Isso é conversão implícita — mas explicitamente declarada no código.


Métodos úteis de Result

Result tem vários métodos que tornam o código mais expressivo sem precisar sempre de match:

fn main() {
    let ok: Result<i32, &str> = Ok(42);
    let err: Result<i32, &str> = Err("algo deu errado");

    // unwrap_or — valor padrão em caso de erro
    println!("{}", ok.unwrap_or(0));   // 42
    println!("{}", err.unwrap_or(0));  // 0

    // unwrap_or_else — valor padrão calculado
    println!("{}", err.unwrap_or_else(|e| {
        println!("Aviso: {e}");
        -1
    }));

    // map — transforma o valor dentro de Ok
    let dobrado = ok.map(|v| v * 2);
    println!("{:?}", dobrado); // Ok(84)

    // map_err — transforma o erro dentro de Err
    let novo_err = err.map_err(|e| format!("Falha: {e}"));
    println!("{:?}", novo_err); // Err("Falha: algo deu errado")

    // is_ok e is_err — verificações booleanas
    println!("{} {}", ok.is_ok(), ok.is_err()); // true false
}

unwrap e expect — quando você tem certeza

Em alguns contextos — protótipos, testes, situações onde o erro é genuinamente impossível — você pode querer desempacotar o resultado sem tratar o erro:

fn main() {
    // unwrap: entra em panic se for Err
    let valor = Ok::<i32, &str>(42).unwrap();
    println!("{valor}"); // 42

    // expect: como unwrap, mas com mensagem personalizada
    let config = std::env::var("HOME")
        .expect("Variável HOME não encontrada");
    println!("{config}");
}

unwrap e expect causam panic! se o resultado for Err. Use-os com parcimônia em código de produção — eles são atalhos legítimos durante desenvolvimento, mas em bibliotecas e sistemas críticos prefira sempre propagar o erro com ? ou tratá-lo explicitamente.

A diferença entre unwrap e expect é apenas a mensagem de panic: expect permite que você explique por que aquele ponto nunca deveria ser um Err, tornando o código mais autodocumentado.


Result em main

A função main também pode retornar Result, o que permite usar ? diretamente nela:

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

fn main() -> Result<(), io::Error> {
    let conteudo = fs::read_to_string("dados.txt")?;
    println!("Linhas: {}", conteudo.lines().count());
    Ok(())
}

Se ocorrer um erro, o programa termina com uma mensagem descritiva e código de saída não-zero — comportamento adequado para ferramentas de linha de comando.


Option vs Result — quando usar cada um

Uma dúvida comum é quando usar Option<T> e quando usar Result<T, E>:

Use Option quando a ausência de valor é esperada e normal — buscar uma chave num mapa que pode não existir, encontrar o primeiro elemento que satisfaz uma condição, acessar o primeiro item de uma lista vazia. A ausência não é um erro — é uma resposta válida.

Use Result quando a falha representa algo que deu errado — leitura de arquivo, parsing de dados, conexão de rede. O erro carrega informação sobre o que falhou e por quê.

// Option: ausência é normal
fn buscar_usuario(id: u32) -> Option<String> {
    if id == 1 { Some(String::from("Ana")) } else { None }
}

// Result: falha tem causa
fn parse_idade(s: &str) -> Result<u32, String> {
    s.parse::<u32>().map_err(|_| format!("'{s}' não é uma idade válida"))
}

fn main() {
    // Option tratado com if let
    if let Some(nome) = buscar_usuario(1) {
        println!("Encontrado: {nome}");
    }

    // Result tratado com match
    match parse_idade("vinte") {
        Ok(idade) => println!("Idade: {idade}"),
        Err(e)    => println!("Erro: {e}"),
    }
}

Um programa completo: parser de configuração

Vamos construir um parser simples de arquivo de configuração no formato chave=valor:

use std::collections::HashMap;
use std::fs;
use std::io;

#[derive(Debug)]
enum ErroConfig {
    Io(io::Error),
    FormatoInvalido(String),
}

impl From<io::Error> for ErroConfig {
    fn from(e: io::Error) -> ErroConfig {
        ErroConfig::Io(e)
    }
}

fn parse_config(caminho: &str) -> Result<HashMap<String, String>, ErroConfig> {
    let conteudo = fs::read_to_string(caminho)?;
    let mut mapa = HashMap::new();

    for (numero, linha) in conteudo.lines().enumerate() {
        let linha = linha.trim();

        if linha.is_empty() || linha.starts_with('#') {
            continue; // ignora linhas vazias e comentários
        }

        match linha.split_once('=') {
            Some((chave, valor)) => {
                mapa.insert(
                    chave.trim().to_string(),
                    valor.trim().to_string(),
                );
            }
            None => {
                return Err(ErroConfig::FormatoInvalido(format!(
                    "Linha {}: '{}' não contém '='",
                    numero + 1,
                    linha
                )));
            }
        }
    }

    Ok(mapa)
}

fn main() {
    match parse_config("app.conf") {
        Ok(config) => {
            println!("Configuração carregada:");
            for (chave, valor) in &config {
                println!("  {chave} = {valor}");
            }
        }
        Err(ErroConfig::Io(e)) => {
            println!("Erro ao ler arquivo: {e}");
        }
        Err(ErroConfig::FormatoInvalido(msg)) => {
            println!("Formato inválido: {msg}");
        }
    }
}

Para testar, crie um arquivo app.conf:

# Configurações da aplicação
host = localhost
porta = 8080
debug = true

A filosofia por trás de Result

Linguagens com exceções criam um contrato implícito: qualquer função pode falhar, mas você não sabe quais sem ler a documentação — ou descobrir em produção. Rust inverte isso. O tipo de retorno de uma função é sua documentação de erros. Se retorna Result, pode falhar. Se retorna T direto, não pode. O contrato é explícito, verificado pelo compilador, e impossível de ignorar acidentalmente.

Isso tem um custo: mais código para escrever, mais decisões a tomar. Mas o resultado é software onde os caminhos de erro são tão bem pensados quanto os caminhos de sucesso — e isso faz toda a diferença em sistemas que precisam ser confiáveis.


Fontes e leituras recomendadas


Comentários

Mais em Rust

Testes — Escrevendo Código que Prova que Seu Código Funciona
Testes — Escrevendo Código que Prova que Seu Código Funciona

&nbsp; Rust tem suporte nativo a testes &mdash; sem frameworks externos, sem...

Lifetimes — Quando o Compilador Precisa de Mais Informação
Lifetimes — Quando o Compilador Precisa de Mais Informação

&nbsp; Chegamos ao conceito que mais intimida quem est&aacute; aprendendo Ru...

Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores
Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores

Se voc&ecirc; vem de Python, JavaScript ou Java, j&aacute; sabe o que &eacute...