Result<T,E> em Rust: Ok e Err

Guia completo de Result<T,E> em Rust: propagação de erros com ?, map, map_err, tipos de erro customizados e padrões práticos.

O que é Result<T,E>

Result<T, E> é o tipo que Rust usa para representar operações que podem falhar de forma recuperável. Ele tem duas variantes: Ok(T) para o caso de sucesso e Err(E) para o caso de erro. Diferente de exceções em outras linguagens, o Result torna os erros explícitos no tipo de retorno — o compilador obriga você a lidar com eles.

Use Result sempre que uma operação pode falhar por motivos previsíveis: leitura de arquivos, parsing de dados, requisições de rede, validação de entrada do usuário. Para erros que indicam bugs no programa (índice fora dos limites, estado impossível), Rust usa panic!. Para valores opcionais sem erro, use Option<T>.


Criando e Usando Result

use std::num::ParseIntError;

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

fn main() {
    // Pattern matching — forma mais completa
    match dividir(10.0, 3.0) {
        Ok(resultado) => println!("Resultado: {resultado:.2}"),
        Err(erro) => println!("Erro: {erro}"),
    }

    // if let — quando só interessa um caso
    if let Err(e) = dividir(10.0, 0.0) {
        eprintln!("Erro: {e}");
    }

    // let-else para early return
    let Ok(valor) = dividir(10.0, 2.0) else {
        eprintln!("Falhou!");
        return;
    };
    println!("Valor: {valor}");

    // parse() retorna Result
    let numero: Result<i32, ParseIntError> = "42".parse();
    println!("{numero:?}"); // Ok(42)

    let invalido: Result<i32, ParseIntError> = "abc".parse();
    println!("{invalido:?}"); // Err(invalid digit found in string)
}

Tabela de Métodos Principais

MétodoDescriçãoExemplo
is_ok()Retorna true se é OkOk(1).is_ok()true
is_err()Retorna true se é ErrErr("x").is_err()true
ok()Converte para Option<T>Ok(1).ok()Some(1)
err()Converte para Option<E>Err("x").err()Some("x")
unwrap()Extrai o valor ou panicOk(1).unwrap()1
unwrap_err()Extrai o erro ou panicErr("x").unwrap_err()"x"
unwrap_or(default)Extrai ou retorna defaultErr("x").unwrap_or(0)0
unwrap_or_else(f)Extrai ou calcula via closureErr("x").unwrap_or_else(|_| 0)
unwrap_or_default()Extrai ou usa DefaultErr("x").unwrap_or_default()0
expect(msg)Como unwrap com mensagemres.expect("falhou")
map(f)Transforma o valor OkOk(1).map(|x| x * 2)Ok(2)
map_err(f)Transforma o valor ErrErr(1).map_err(|e| e + 1)Err(2)
and_then(f)Encadeia operações ResultOk(1).and_then(|x| Ok(x + 1))
or_else(f)Alternativa se for ErrErr(1).or_else(|_| Ok(0))
as_ref()Result<&T, &E> a partir de &Resultres.as_ref()
inspect(f)Executa closure sem consumir (Rust 1.76+)res.inspect(|v| println!("{v}"))
inspect_err(f)Inspeciona o erro sem consumirres.inspect_err(|e| log::error!("{e}"))

Exemplos Práticos

1. Propagação de Erros com ?

O operador ? é a forma idiomática de propagar erros em Rust. Ele extrai o valor de Ok ou retorna Err imediatamente da função.

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

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

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

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

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

fn main() {
    match ler_numero_do_arquivo("numero.txt") {
        Ok(n) => println!("Número lido: {n}"),
        Err(AppErro::Io(e)) => eprintln!("Erro de I/O: {e}"),
        Err(AppErro::Parse(e)) => eprintln!("Erro de parse: {e}"),
    }
}

2. Transformando Erros com map_err

fn validar_idade(input: &str) -> Result<u8, String> {
    let idade: u8 = input
        .trim()
        .parse()
        .map_err(|e| format!("'{input}' não é uma idade válida: {e}"))?;

    if idade < 1 || idade > 150 {
        return Err(format!("Idade {idade} fora do intervalo permitido (1-150)"));
    }

    Ok(idade)
}

fn main() {
    let testes = vec!["25", "abc", "200", "0", "30"];

    for entrada in testes {
        match validar_idade(entrada) {
            Ok(idade) => println!("{entrada} → Idade válida: {idade}"),
            Err(e) => println!("{entrada} → Erro: {e}"),
        }
    }
}

3. Coletando Results em um Vec

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

    // collect() que falha no primeiro erro
    let todos: Result<Vec<i32>, _> = entradas
        .iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("Todos: {todos:?}"); // Ok([1, 2, 3, 4, 5])

    // Com um valor inválido
    let com_erro: Result<Vec<i32>, _> = vec!["1", "abc", "3"]
        .iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("Com erro: {com_erro:?}"); // Err(...)

    // Separando sucessos e falhas com partition
    let misturado = vec!["10", "xyz", "20", "abc", "30"];
    let (ok, erros): (Vec<_>, Vec<_>) = misturado
        .iter()
        .map(|s| s.parse::<i32>())
        .partition(Result::is_ok);

    let numeros: Vec<i32> = ok.into_iter().map(Result::unwrap).collect();
    let falhas: Vec<String> = erros
        .into_iter()
        .map(|e| e.unwrap_err().to_string())
        .collect();

    println!("Números: {numeros:?}");  // [10, 20, 30]
    println!("Erros: {falhas:?}");
}

4. Tipo de Erro Customizado com thiserror

// Usando derivação manual (sem crate externo)
use std::fmt;
use std::io;

#[derive(Debug)]
enum ConfigErro {
    ArquivoNaoEncontrado(io::Error),
    ChaveAusente(String),
    ValorInvalido { chave: String, valor: String },
}

impl fmt::Display for ConfigErro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigErro::ArquivoNaoEncontrado(e) => {
                write!(f, "Arquivo de configuração não encontrado: {e}")
            }
            ConfigErro::ChaveAusente(chave) => {
                write!(f, "Chave obrigatória ausente: '{chave}'")
            }
            ConfigErro::ValorInvalido { chave, valor } => {
                write!(f, "Valor inválido '{valor}' para a chave '{chave}'")
            }
        }
    }
}

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

impl From<io::Error> for ConfigErro {
    fn from(e: io::Error) -> Self {
        ConfigErro::ArquivoNaoEncontrado(e)
    }
}

fn carregar_porta(config: &std::collections::HashMap<String, String>) -> Result<u16, ConfigErro> {
    let valor = config
        .get("porta")
        .ok_or_else(|| ConfigErro::ChaveAusente("porta".to_string()))?;

    valor.parse::<u16>().map_err(|_| ConfigErro::ValorInvalido {
        chave: "porta".to_string(),
        valor: valor.clone(),
    })
}

fn main() {
    use std::collections::HashMap;

    let mut config = HashMap::new();
    config.insert("porta".to_string(), "8080".to_string());

    match carregar_porta(&config) {
        Ok(porta) => println!("Porta: {porta}"),
        Err(e) => eprintln!("Erro: {e}"),
    }

    // Testando com valor inválido
    config.insert("porta".to_string(), "abc".to_string());
    match carregar_porta(&config) {
        Ok(porta) => println!("Porta: {porta}"),
        Err(e) => eprintln!("Erro: {e}"),
    }
}

5. Encadeamento Fluente com and_then

fn parse_coordenada(input: &str) -> Result<(f64, f64), String> {
    let partes: Vec<&str> = input.split(',').collect();

    if partes.len() != 2 {
        return Err(format!("Esperado 'lat,lon', recebido: '{input}'"));
    }

    let lat = partes[0].trim().parse::<f64>()
        .map_err(|e| format!("Latitude inválida: {e}"))?;
    let lon = partes[1].trim().parse::<f64>()
        .map_err(|e| format!("Longitude inválida: {e}"))?;

    validar_latitude(lat)
        .and_then(|lat| validar_longitude(lon).map(|lon| (lat, lon)))
}

fn validar_latitude(lat: f64) -> Result<f64, String> {
    if (-90.0..=90.0).contains(&lat) {
        Ok(lat)
    } else {
        Err(format!("Latitude {lat} fora do intervalo [-90, 90]"))
    }
}

fn validar_longitude(lon: f64) -> Result<f64, String> {
    if (-180.0..=180.0).contains(&lon) {
        Ok(lon)
    } else {
        Err(format!("Longitude {lon} fora do intervalo [-180, 180]"))
    }
}

fn main() {
    let testes = vec![
        "-23.5505, -46.6333",   // São Paulo
        "91.0, 0.0",            // Latitude inválida
        "abc, 10.0",            // Parse error
        "0.0",                  // Formato inválido
    ];

    for entrada in testes {
        match parse_coordenada(entrada) {
            Ok((lat, lon)) => println!("({lat}, {lon}) - Válido"),
            Err(e) => println!("'{entrada}' - Erro: {e}"),
        }
    }
}

Dicas de Performance e Armadilhas

  1. Use ? ao invés de match para propagação: O operador ? é mais conciso e idiomático. Ele também chama From::from automaticamente para converter tipos de erro.

  2. Evite unwrap() em código de produção: Use expect() no mínimo (para documentar por que o valor deveria ser Ok) ou, melhor ainda, propague o erro com ?.

  3. Box<dyn Error> para prototipagem rápida: Em main() ou scripts rápidos, use Result<T, Box<dyn std::error::Error>> para aceitar qualquer tipo de erro sem criar enums personalizados.

  4. map_err para contexto adicional: Sempre que propagar um erro, considere usar map_err para adicionar contexto sobre o que estava sendo feito quando o erro ocorreu.

  5. Result é um iterador: Result implementa IntoIteratorOk(v) produz um iterador com um elemento, Err produz um iterador vazio. Isso é útil com flat_map e chain.

  6. ? em main(): Desde Rust 1.26, main() pode retornar Result. Basta declarar fn main() -> Result<(), Box<dyn std::error::Error>>.


Veja Também