teoria

Tratamento de Erros com Option e Result

Aprenda sobre tratamento de erros com option e result

30 min
Aula 4 de 5

Tratamento de Erros com Option e Result

Olá, estudante de Rust! 👋 Sejam bem-vindos a mais uma aula do nosso curso. Hoje, mergulharemos em um dos pilares da robustez do Rust: o tratamento de erros. Em Rust, a forma como lidamos com falhas e valores ausentes é fundamental para escrever código seguro, confiável e, acima de tudo, que não cause surpresas inesperadas em tempo de execução.

Diferente de muitas linguagens que utilizam exceções ou valores null/nil, Rust adota uma abordagem diferente, focada em tipos enumerados (enum) que representam explicitamente a possibilidade de sucesso ou falha. Isso nos força a considerar e tratar esses cenários, tornando o código mais previsível e menos propenso a bugs.

Nesta aula, exploraremos dois enums cruciais: Option<T> e Result<T, E>. Eles são a espinha dorsal do tratamento de erros em Rust e são amplamente utilizados em toda a biblioteca padrão e em crates de terceiros.

Vamos começar! 🚀


1. Option<T>: Lidando com a Ausência de Valores

O enum Option<T> é usado quando um valor pode ou não estar presente. É a resposta de Rust para o problema do null em outras linguagens, mas de uma forma muito mais segura e expressiva.

1.1. O que é Option<T>?

Imagine que você está procurando por um item em uma lista. O item pode estar lá, ou não. Option<T> representa exatamente essa dualidade. Ele é definido na biblioteca padrão do Rust da seguinte forma:

enum Option<T> {
    None,      // Indica que não há valor.
    Some(T),   // Indica que há um valor do tipo T.
}

Onde T é um tipo genérico, significando que Option pode conter qualquer tipo de dado (um i32, uma String, uma struct personalizada, etc.).

1.2. Quando usar Option<T>?

Você usará Option<T> sempre que:

  • Uma função pode não retornar um valor significativo.
  • Um campo de uma struct pode ser opcional.
  • Você está pesquisando algo que pode não existir (ex: HashMap::get).
  • Um valor pode ser null em outra linguagem (mas em Rust, ele será um Option).

1.3. Exemplos de Uso e Tratamento

A maneira mais comum e segura de interagir com um Option<T> é usando o match expression. Isso nos permite executar diferentes blocos de código dependendo se o Option é Some ou None.

Exemplo 1: Buscando um elemento em um vetor

fn main() {
    let numeros = vec![1, 2, 3, 4, 5];
 
    // Tenta encontrar o número 3
    let talvez_tres = numeros.iter().find(|&&x| x == 3);
 
    // Tenta encontrar o número 6
    let talvez_seis = numeros.iter().find(|&&x| x == 6);
 
    println!("Buscando o 3: {:?}", talvez_tres); // Saída: Buscando o 3: Some(3)
    println!("Buscando o 6: {:?}", talvez_seis); // Saída: Buscando o 6: None
 
    // Tratando com match
    match talvez_tres {
        Some(valor) => println!("Encontrei o número: {}", valor),
        None => println!("O número não foi encontrado."),
    }
 
    match talvez_seis {
        Some(valor) => println!("Encontrei o número: {}", valor),
        None => println!("O número não foi encontrado."),
    }
}

Exemplo 2: Parseando um número (do livro oficial do Rust)

fn main() {
    let guess = "42".parse::<i32>(); // Retorna Result<i32, ParseIntError>
 
    // Para simplificar o exemplo de Option, vamos converter para Option
    // (normalmente você usaria Result diretamente aqui)
    let maybe_number: Option<i32> = match guess {
        Ok(num) => Some(num),
        Err(_) => None,
    };
 
    match maybe_number {
        Some(num) => println!("O número é: {}", num),
        None => println!("Não foi possível converter para número."),
    }
 
    let invalid_guess = "abc".parse::<i32>();
    let maybe_invalid_number: Option<i32> = match invalid_guess {
        Ok(num) => Some(num),
        Err(_) => None,
    };
 
    match maybe_invalid_number {
        Some(num) => println!("O número é: {}", num),
        None => println!("Não foi possível converter para número."),
    }
}

Métodos úteis para Option<T>

  • is_some() e is_none(): Retornam true ou false.
  • unwrap(): Retorna o valor dentro de Some, ou entra em pânico (panic!) se for None. Evite usar em código de produção.
  • expect("mensagem de erro"): Semelhante a unwrap(), mas permite uma mensagem de erro personalizada antes do panic!. Evite usar em código de produção.
  • unwrap_or(default_value): Retorna o valor dentro de Some, ou um valor padrão se for None.
  • map(closure): Se for Some(T), aplica a closure ao valor T e retorna um novo Option<U>. Se for None, retorna None.
  • and_then(closure): Semelhante a map, mas a closure deve retornar um Option. Útil para encadear operações que podem falhar.
fn main() {
    let some_value = Some(10);
    let none_value: Option<i32> = None;
 
    // unwrap_or
    let x = some_value.unwrap_or(0); // x é 10
    let y = none_value.unwrap_or(0); // y é 0
    println!("unwrap_or: x={}, y={}", x, y);
 
    // map
    let doubled_some = some_value.map(|val| val * 2); // Some(20)
    let doubled_none = none_value.map(|val| val * 2); // None
    println!("map: some={:?}, none={:?}", doubled_some, doubled_none);
 
    // and_then (flat_map em outras linguagens)
    fn divide_by_two(n: i32) -> Option<i32> {
        if n % 2 == 0 {
            Some(n / 2)
        } else {
            None
        }
    }
 
    let result1 = Some(10).and_then(divide_by_two); // Some(5)
    let result2 = Some(7).and_then(divide_by_two);  // None
    let result3 = None.and_then(divide_by_two);     // None
    println!("and_then: result1={:?}, result2={:?}, result3={:?}", result1, result2, result3);
 
    // if let (shorthand para match)
    if let Some(val) = some_value {
        println!("Usando if let: O valor é {}", val);
    } else {
        println!("Usando if let: Não há valor.");
    }
}

2. Result<T, E>: Lidando com Operações que Podem Falhar

O enum Result<T, E> é a principal ferramenta de Rust para lidar com erros recuperáveis. Ele representa o resultado de uma operação que pode ter sucesso e retornar um valor do tipo T, ou falhar e retornar um erro do tipo E.

2.1. O que é Result<T, E>?

Pense em uma operação que pode dar errado, como abrir um arquivo. Ela pode ter sucesso e retornar o arquivo aberto, ou falhar (arquivo não encontrado, permissão negada) e retornar um erro. Result<T, E> modela isso. Sua definição é:

enum Result<T, E> {
    Ok(T),    // Indica sucesso e contém o valor resultante do tipo T.
    Err(E),   // Indica falha e contém a informação do erro do tipo E.
}

Aqui, T é o tipo do valor de sucesso e E é o tipo do valor de erro. Ambos são genéricos, permitindo flexibilidade total.

2.2. Quando usar Result<T, E>?

Você usará Result<T, E> sempre que:

  • Uma função pode falhar de uma forma que o chamador pode querer tratar (ex: I/O, parsing, validação).
  • Você precisa fornecer informações detalhadas sobre a falha.
  • Uma função em outra linguagem levantaria uma exceção.

2.3. Exemplos de Uso e Tratamento

Assim como Option, a forma mais comum de tratar um Result é com match.

Exemplo 1: Abrindo um arquivo (do livro oficial do Rust)

use std::fs::File;
use std::io::ErrorKind; // Para comparar tipos de erro específicos
 
fn main() {
    let f = File::open("hello.txt"); // Tenta abrir um arquivo
 
    let f = match f {
        Ok(file) => {
            println!("Arquivo aberto com sucesso!");
            file
        },
        Err(error) => match error.kind() {
            ErrorKind::NotFound => {
                // Se o arquivo não existe, tenta criar
                match File::create("hello.txt") {
                    Ok(fc) => {
                        println!("Arquivo criado com sucesso!");
                        fc
                    },
                    Err(e) => {
                        // Não foi possível criar o arquivo
                        panic!("Problema ao criar o arquivo: {:?}", e);
                    },
                }
            },
            other_error => {
                // Outro tipo de erro ao abrir
                panic!("Problema ao abrir o arquivo: {:?}", other_error);
            },
        },
    };
 
    // Agora 'f' é um File handle, e podemos usá-lo
    println!("File handle: {:?}", f);
}

Este exemplo mostra como podemos encadear match expressions para lidar com diferentes tipos de erros.

Métodos úteis para Result<T, E>

Assim como Option, Result também possui métodos que facilitam o tratamento:

  • is_ok() e is_err(): Retornam true ou false.
  • unwrap(): Retorna o valor dentro de Ok, ou entra em pânico (panic!) se for Err. Evite usar em código de produção.
  • expect("mensagem de erro"): Semelhante a unwrap(), mas com uma mensagem de erro personalizada. Evite usar em código de produção.
  • unwrap_or(default_value): Retorna o valor dentro de Ok, ou um valor padrão se for Err.
  • map(closure): Se for Ok(T), aplica a closure ao valor T e retorna um novo Result<U, E>. Se for Err(E), retorna Err(E).
  • map_err(closure): Se for Err(E), aplica a closure ao erro E e retorna um novo Result<T, F>. Se for Ok(T), retorna Ok(T).
  • and_then(closure): Semelhante a map, mas a closure deve retornar um Result. Útil para encadear operações que podem falhar.
use std::fs::File;
use std::io::{self, Read};
 
fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?; // O operador '?' simplifica o tratamento de erros!
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
 
fn main() {
    // Exemplo de map e map_err
    let parse_result: Result<i32, _> = "10".parse();
    let mapped_ok = parse_result.map(|n| n * 2); // Ok(20)
    println!("Mapped Ok: {:?}", mapped_ok);
 
    let parse_error: Result<i32, _> = "abc".parse();
    let mapped_err = parse_error.map_err(|e| format!("Erro de parse: {}", e)); // Err("Erro de parse: invalid digit found in string")
    println!("Mapped Err: {:?}", mapped_err);
 
    // Exemplo de and_then
    fn get_user_id(username: &str) -> Result<u32, String> {
        if username == "alice" { Ok(123) } else { Err("Usuário não encontrado".to_string()) }
    }
 
    fn get_user_profile(id: u32) -> Result<String, String> {
        if id == 123 { Ok("Perfil da Alice".to_string()) } else { Err("Perfil não encontrado".to_string()) }
    }
 
    let alice_profile = get_user_id("alice").and_then(get_user_profile); // Ok("Perfil da Alice")
    let bob_profile = get_user_id("bob").and_then(get_user_profile);     // Err("Usuário não encontrado")
    println!("Alice profile: {:?}", alice_profile);
    println!("Bob profile: {:?}", bob_profile);
 
    // Exemplo de read_username_from_file com '?'
    match read_username_from_file() {
        Ok(username) => println!("Nome de usuário: {}", username),
        Err(e) => println!("Erro ao ler nome de usuário: {}", e),
    }
}

3. Propagação de Erros com o Operador ? (Question Mark Operator)

O operador ? é uma conveniência sintática poderosa para propagar erros de Result (e Option, embora menos comum) para a função chamadora. Ele simplifica drasticamente o código que lida com múltiplos Results encadeados.

3.1. Como funciona o ?

Quando você usa expression? em uma função que retorna Result<T, E>:

  1. Se expression for Ok(v), o valor v é extraído e a execução continua.
  2. Se expression for Err(e), o erro e é retornado imediatamente da função atual.

Isso é equivalente a um match expression:

// Código com '?'
let f = File::open("hello.txt")?;
 
// Equivalente com 'match'
let f = match File::open("hello.txt") {
    Ok(file) => file,
    Err(e) => return Err(e), // Retorna o erro imediatamente
};

Importante: O operador ? só pode ser usado em funções que retornam um Result (ou Option). Se sua função main precisa usar ?, ela deve ter a assinatura fn main() -> Result<(), Box<dyn Error>>.

3.2. Exemplo: Leitura de arquivo com ?

Vamos reescrever o exemplo de leitura de nome de usuário para mostrar o poder do ?.

use std::fs::File;
use std::io::{self, Read}; // Importa io::Read para usar o trait read_to_string
use std::error::Error; // Para o tipo de retorno de main
 
// Função para ler um nome de usuário de um arquivo
fn read_username_from_file_with_q_operator() -> Result<String, io::Error> {
    // Tenta abrir o arquivo. Se falhar, retorna o erro.
    let mut f = File::open("username.txt")?;
 
    let mut s = String::new();
    // Tenta ler o conteúdo para a string. Se falhar, retorna o erro.
    f.read_to_string(&mut s)?;
 
    // Se tudo deu certo, retorna a string encapsulada em Ok
    Ok(s)
}
 
// A função main pode retornar um Result para usar '?'
fn main() -> Result<(), Box<dyn Error>> { // Box<dyn Error> é um tipo comum para erros genéricos
    // Para que o exemplo funcione, vamos criar o arquivo primeiro
    let _ = File::create("username.txt")?.write_all(b"john_doe");
    
    // Tenta ler o nome de usuário. Se falhar, o '?' propagará o erro de main.
    let username = read_username_from_file_with_q_operator()?;
    println!("Nome de usuário lido: {}", username);
 
    // Exemplo de como um erro seria propagado
    // Vamos tentar abrir um arquivo que não existe para ver o erro
    let _ = File::open("non_existent_file.txt")?;
    
    Ok(()) // Retorna Ok(()) se tudo der certo
}

No exemplo acima, se File::open("username.txt") falhar, o erro é imediatamente retornado da função read_username_from_file_with_q_operator. Se read_to_string falhar, o mesmo acontece. Isso torna o código muito mais limpo e conciso.


4. Boas Práticas e Integração

4.1. Quando usar Option vs. Result? 🤔

  • Option<T>: Use quando a ausência de um valor é uma condição esperada e válida que não indica uma falha de operação. Ex: Um item pode ou não estar em um mapa; uma função pode não ter um valor para retornar em certas circunstâncias. Não há "erro", apenas "nada".
  • Result<T, E>: Use quando uma operação pode falhar de uma forma que o chamador pode querer tratar ou que representa uma condição de erro. Ex: Falha ao abrir um arquivo, erro de rede, falha na validação de entrada. A falha tem uma causa específica (E).

4.2. Evite unwrap() e expect() em Produção! 🚨

Embora unwrap() e expect() sejam úteis para prototipagem e testes, eles causam um panic! se encontrarem um None ou Err. Em código de produção, um panic! geralmente significa que seu programa trava. Sempre prefira tratar os casos None e Err explicitamente com match, if let, ou métodos como unwrap_or, map, and_then, ou o operador ?.

4.3. Encadear Operações com map, and_then, or_else

Esses métodos permitem um estilo de programação mais funcional e conciso para lidar com Option e Result, evitando match aninhados e tornando o fluxo de dados mais claro.

fn get_config_value(key: &str) -> Option<String> {
    // Simula buscar um valor de configuração
    match key {
        "timeout" => Some("1000".to_string()),
        "max_retries" => Some("3".to_string()),
        _ => None,
    }
}
 
fn parse_and_double_timeout() -> Option<u32> {
    get_config_value("timeout")
        .and_then(|s| s.parse::<u32>().ok()) // .ok() converte Result para Option
        .map(|t| t * 2)
}
 
fn main() {
    match parse_and_double_timeout() {
        Some(timeout) => println!("Timeout dobrado: {}", timeout),
        None => println!("Não foi possível obter ou dobrar o timeout."),
    }
 
    // Exemplo com Result
    fn divide(a: i32, b: i32) -> Result<i32, String> {
        if b == 0 { Err("Divisão por zero!".to_string()) } else { Ok(a / b) }
    }
 
    fn multiply_by_ten(n: i32) -> Result<i32, String> {
        Ok(n * 10)
    }
 
    let result_ok = divide(10, 2).and_then(multiply_by_ten); // Ok(50)
    let result_err = divide(10, 0).and_then(multiply_by_ten); // Err("Divisão por zero!".to_string())
 
    println!("Result Ok: {:?}", result_ok);
    println!("Result Err: {:?}", result_err);
}

5. Resumo e Próximos Passos

Nesta aula, exploramos as ferramentas fundamentais de tratamento de erros em Rust:

  • Option<T>: Para valores que podem estar ausentes (Some(T) ou None). Essencial para evitar o "erro de referência nula".
  • Result<T, E>: Para operações que podem falhar (Ok(T) para sucesso, Err(E) para falha). A principal forma de lidar com erros recuperáveis.
  • Operador ?: Uma conveniência sintática para propagar erros de Result para a função chamadora, tornando o código mais limpo.

Dominar Option e Result é crucial para escrever código Rust idiomático e robusto. Lembre-se sempre de tratar os casos de sucesso e falha explicitamente, evitando unwrap() e expect() em produção.

Próximos Passos 🚀

No futuro, você pode explorar:

  • Tipos de Erro Personalizados: Como criar seus próprios enums de erro para fornecer informações mais detalhadas sobre falhas.
  • Crates de Tratamento de Erros: Bibliotecas como anyhow e thiserror que simplificam a criação e manipulação de erros personalizados e genéricos.
  • Combinadores de Option e Result: Existem muitos outros métodos úteis para encadear e transformar Options e Results de maneiras elegantes.

Parabéns por avançar em sua jornada Rust! O tratamento de erros é um tópico complexo, mas a abordagem de Rust o torna muito mais seguro e gerenciável. Continue praticando!

© 2025 Escola All Dev. Todos os direitos reservados.

Tratamento de Erros com Option e Result - Curso gratuito de Rust: A linguagem mais amada | escola.all.dev.br