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
- The Rust Programming Language, Cap. 9 — Error Handling — https://doc.rust-lang.org/book/ch09-00-error-handling.html
- Rust by Example — Error Handling — https://doc.rust-lang.org/rust-by-example/error.html
- The
thiserrorcrate — forma idiomática de definir tipos de erro customizados — https://docs.rs/thiserror - The
anyhowcrate — tratamento de erros simplificado para aplicações — https://docs.rs/anyhow - "Error Handling in Rust" — Andrew Gallant (BurntSushi) — artigo aprofundado — https://blog.burntsushi.net/rust-error-handling/
- Rustlings, seção
error_handling— https://github.com/rust-lang/rustlings