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ébricos — Result<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:
- Erros recuperáveis — Situações esperadas que o programa pode tratar (arquivo não encontrado, input inválido). Representados por
Result<T, E>. - 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 paraResult<T, anyhow::Error>, aceita qualquer tipo de erro.context()— Adiciona contexto a um erro (muito útil para debugging)bail!— Retorna um erro imediatamente (como umreturn Err(...)mais conciso)ensure!— Verifica uma condição e retorna erro se falsa
thiserror vs. anyhow: Quando Usar Cada Um
| Cenário | Use | Por quê |
|---|---|---|
| Biblioteca (crate pública) | thiserror | Usuários precisam de tipos de erro bem definidos para tratá-los |
| Aplicação (binário) | anyhow | Simplicidade; erros geralmente são reportados ao usuário |
| Código interno complexo | Ambos | thiserror 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
- Nunca use
unwrap()em código de produção — a menos que tenha 100% de certeza de que o valor existe - Prefira
?em vez dematchpara propagação — é mais legível - Use
context()do anyhow — facilita muito o debugging em produção - Crie tipos de erro específicos para bibliotecas com
thiserror - Use
anyhowpara aplicações — menos boilerplate, mais produtividade - Erros devem ser informativos — inclua contexto sobre o que aconteceu e por quê
- Documente quais erros uma função pode retornar — ajuda quem usa seu código
Veja Também
- Boas Práticas de Error Handling em Rust — padrões avançados para projetos de produção
- anyhow e thiserror: Quando Usar Cada Um — guia detalhado das crates mais populares para erros
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!