O tratamento de erros em Rust é radicalmente diferente de outras linguagens. Não há exceções (exceptions), não há try/catch, não há null. Em vez disso, Rust usa dois tipos algébricos poderosos: Result<T, E> para operações que podem falhar, e Option<T> para valores que podem estar ausentes. Combinados com o operador ?, traits de conversão e crates como thiserror e anyhow, o sistema de erros do Rust é o mais robusto e ergonômico que existe.
A filosofia do Rust é clara: erros são valores, não fluxos de controle excepcionais. Todo erro deve ser tratado explicitamente, e o compilador garante que nenhum caso de erro seja ignorado.
Problema
Em linguagens com exceções, erros podem surgir de qualquer lugar e propagar silenciosamente:
# Python: qualquer linha pode lançar uma exceção
def processar():
arquivo = abrir("dados.txt") # IOError?
dados = json.loads(arquivo.read()) # JSONDecodeError?
validar(dados) # ValueError?
salvar_no_banco(dados) # DatabaseError?
# Quais exceções esta função pode lançar? Ninguém sabe!
O chamador não sabe quais erros esperar. Exceções não checadas propagam silenciosamente, causando crashes em produção.
Solução em Rust
Result e Option — A Base
/// Result: operação que pode falhar
fn dividir(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Divisão por zero".to_string())
} else {
Ok(a / b)
}
}
/// Option: valor que pode estar ausente
fn buscar_usuario(id: u64) -> Option<String> {
match id {
1 => Some("Alice".to_string()),
2 => Some("Bob".to_string()),
_ => None,
}
}
fn main() {
// Tratamento explícito com match
match dividir(10.0, 3.0) {
Ok(resultado) => println!("10/3 = {:.2}", resultado),
Err(erro) => println!("Erro: {}", erro),
}
// Métodos de conveniência
let resultado = dividir(10.0, 0.0).unwrap_or(0.0);
println!("Com fallback: {}", resultado);
// map para transformar o valor de sucesso
let dobro = dividir(10.0, 2.0).map(|v| v * 2.0);
println!("Dobro: {:?}", dobro); // Ok(10.0)
// and_then para encadear operações falíveis
let resultado = dividir(100.0, 5.0)
.and_then(|v| dividir(v, 2.0))
.and_then(|v| dividir(v, 0.0)); // Falha aqui
println!("Encadeado: {:?}", resultado); // Err("Divisão por zero")
// Option: tratamento de ausência
match buscar_usuario(1) {
Some(nome) => println!("Encontrado: {}", nome),
None => println!("Usuário não encontrado"),
}
// unwrap_or_else com closure
let nome = buscar_usuario(99)
.unwrap_or_else(|| "Anônimo".to_string());
println!("Nome: {}", nome);
// Convertendo entre Option e Result
let resultado: Result<String, String> = buscar_usuario(99)
.ok_or_else(|| "Usuário não encontrado".to_string());
println!("Como Result: {:?}", resultado);
}
O Operador ? — Propagação Elegante
use std::fs;
use std::num::ParseIntError;
/// Sem o operador ? — verboso e repetitivo
fn ler_numero_sem_interrogacao(caminho: &str) -> Result<i32, String> {
let conteudo = match fs::read_to_string(caminho) {
Ok(c) => c,
Err(e) => return Err(format!("Erro de IO: {}", e)),
};
let numero = match conteudo.trim().parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(format!("Erro de parse: {}", e)),
};
Ok(numero)
}
/// Com o operador ? — limpo e conciso
#[derive(Debug)]
enum ErroLeitura {
Io(std::io::Error),
Parse(ParseIntError),
}
impl From<std::io::Error> for ErroLeitura {
fn from(e: std::io::Error) -> Self {
ErroLeitura::Io(e)
}
}
impl From<ParseIntError> for ErroLeitura {
fn from(e: ParseIntError) -> Self {
ErroLeitura::Parse(e)
}
}
impl std::fmt::Display for ErroLeitura {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ErroLeitura::Io(e) => write!(f, "Erro de IO: {}", e),
ErroLeitura::Parse(e) => write!(f, "Erro de parse: {}", e),
}
}
}
/// O operador ? propaga o erro automaticamente,
/// usando as conversões From definidas acima
fn ler_numero(caminho: &str) -> Result<i32, ErroLeitura> {
let conteudo = fs::read_to_string(caminho)?; // io::Error -> ErroLeitura
let numero = conteudo.trim().parse::<i32>()?; // ParseIntError -> ErroLeitura
Ok(numero)
}
/// ? também funciona com Option
fn primeiro_par(numeros: &[i32]) -> Option<i32> {
let primeiro = numeros.first()?; // None se vazio
if primeiro % 2 == 0 {
Some(*primeiro)
} else {
None
}
}
Tipos de Erro Customizados com thiserror
O crate thiserror gera automaticamente as implementações de Display, Error e From:
// Em Cargo.toml: thiserror = "2"
use std::num::ParseIntError;
/// Erros da camada de validação
#[derive(Debug, thiserror::Error)]
enum ErroValidacao {
#[error("Campo '{campo}' é obrigatório")]
CampoObrigatorio { campo: String },
#[error("Email '{0}' tem formato inválido")]
EmailInvalido(String),
#[error("Idade {idade} fora do intervalo [{min}, {max}]")]
IdadeForaDoIntervalo { idade: u32, min: u32, max: u32 },
}
/// Erros da camada de persistência
#[derive(Debug, thiserror::Error)]
enum ErroPersistencia {
#[error("Registro com ID {0} não encontrado")]
NaoEncontrado(u64),
#[error("Violação de unicidade: {campo} = '{valor}'")]
DuplicataDetectada { campo: String, valor: String },
#[error("Erro de conexão: {0}")]
Conexao(String),
}
/// Erro da aplicação — agrega todas as camadas
#[derive(Debug, thiserror::Error)]
enum ErroAplicacao {
#[error("Erro de validação: {0}")]
Validacao(#[from] ErroValidacao),
#[error("Erro de persistência: {0}")]
Persistencia(#[from] ErroPersistencia),
#[error("Erro de IO: {0}")]
Io(#[from] std::io::Error),
#[error("Erro ao parsear número: {0}")]
ParseInt(#[from] ParseIntError),
#[error("Erro interno: {0}")]
Interno(String),
}
/// Validação de dados do usuário
fn validar_usuario(nome: &str, email: &str, idade: u32) -> Result<(), ErroValidacao> {
if nome.is_empty() {
return Err(ErroValidacao::CampoObrigatorio {
campo: "nome".to_string(),
});
}
if !email.contains('@') || !email.contains('.') {
return Err(ErroValidacao::EmailInvalido(email.to_string()));
}
if idade < 13 || idade > 150 {
return Err(ErroValidacao::IdadeForaDoIntervalo {
idade,
min: 13,
max: 150,
});
}
Ok(())
}
/// Salvar no banco de dados
fn salvar_usuario(nome: &str, email: &str) -> Result<u64, ErroPersistencia> {
// Simulando verificação de duplicata
if email == "admin@exemplo.com" {
return Err(ErroPersistencia::DuplicataDetectada {
campo: "email".to_string(),
valor: email.to_string(),
});
}
println!("[BD] Usuário '{}' salvo", nome);
Ok(42) // ID do registro
}
/// Função que integra todas as camadas — usa ? com conversão automática
fn registrar_usuario(
nome: &str,
email: &str,
idade: u32,
) -> Result<u64, ErroAplicacao> {
// ? converte ErroValidacao -> ErroAplicacao automaticamente (via From)
validar_usuario(nome, email, idade)?;
// ? converte ErroPersistencia -> ErroAplicacao automaticamente
let id = salvar_usuario(nome, email)?;
println!("Usuário registrado com ID {}", id);
Ok(id)
}
fn main() {
// Sucesso
match registrar_usuario("Maria", "maria@exemplo.com", 28) {
Ok(id) => println!("Sucesso! ID: {}", id),
Err(e) => println!("Erro: {}", e),
}
println!("---");
// Erro de validação
match registrar_usuario("", "maria@exemplo.com", 28) {
Ok(id) => println!("Sucesso! ID: {}", id),
Err(e) => println!("Erro: {}", e),
}
// Erro de persistência
match registrar_usuario("Admin", "admin@exemplo.com", 30) {
Ok(id) => println!("Sucesso! ID: {}", id),
Err(e) => println!("Erro: {}", e),
}
}
Erros de Aplicação com anyhow
Para aplicações (não bibliotecas), anyhow simplifica o tratamento de erros:
// Em Cargo.toml: anyhow = "1"
use anyhow::{Context, Result, bail, ensure};
/// Com anyhow, Result<T> é equivalente a Result<T, anyhow::Error>
fn carregar_configuracao(caminho: &str) -> Result<Config> {
let conteudo = std::fs::read_to_string(caminho)
.context(format!("Falha ao ler arquivo de configuração '{}'", caminho))?;
let config: Config = serde_json::from_str(&conteudo)
.context("Falha ao parsear configuração JSON")?;
ensure!(config.porta > 0, "Porta deve ser positiva, encontrado: {}", config.porta);
if config.host.is_empty() {
bail!("Host não pode estar vazio");
}
Ok(config)
}
/// Em main(), anyhow formata automaticamente a cadeia de erros
fn main() -> Result<()> {
let config = carregar_configuracao("config.json")
.context("Falha ao inicializar aplicação")?;
println!("Servidor: {}:{}", config.host, config.porta);
Ok(())
}
// Saída em caso de erro:
// Error: Falha ao inicializar aplicação
//
// Caused by:
// 0: Falha ao ler arquivo de configuração 'config.json'
// 1: No such file or directory (os error 2)
struct Config {
host: String,
porta: u16,
}
Diagrama
Hierarquia de Erros — Camadas da Aplicação:
┌─────────────────────────────────────────────┐
│ ErroAplicacao │
│ (enum raiz — agrega todas as camadas) │
│─────────────────────────────────────────────│
│ │
│ ┌──────────────┐ ┌───────────────────┐ │
│ │ErroValidacao │ │ ErroPersistencia │ │
│ │──────────────│ │───────────────────│ │
│ │CampoObrig. │ │NaoEncontrado │ │
│ │EmailInvalido │ │DuplicataDetect. │ │
│ │IdadeForaInt. │ │Conexao │ │
│ └──────┬───────┘ └────────┬──────────┘ │
│ │ From │ From │
│ └─────────┬─────────┘ │
│ ▼ │
│ ErroAplicacao │
│ │
│ ┌──────────────┐ ┌───────────────────┐ │
│ │ std::io::Error│ │ ParseIntError │ │
│ └──────┬───────┘ └────────┬──────────┘ │
│ │ From │ From │
│ └─────────┬─────────┘ │
│ ▼ │
│ ErroAplicacao │
└─────────────────────────────────────────────┘
Operador ? — Fluxo de Propagação:
fn camada_3() -> Result<T, Erro3> { ... }
fn camada_2() -> Result<T, Erro2> {
let val = camada_3()?; ─── Err(e3) ──→ From::from(e3) ──→ Err(Erro2)
Ok(val) ─── Ok(v) ──→ v (continua)
}
fn camada_1() -> Result<T, Erro1> {
let val = camada_2()?; ─── Err(e2) ──→ From::from(e2) ──→ Err(Erro1)
Ok(val)
}
thiserror vs anyhow:
┌─────────────┐ ┌─────────────┐
│ thiserror │ │ anyhow │
│─────────────│ │─────────────│
│ Bibliotecas │ │ Aplicações │
│ Erros tipados│ │ Erros dinâm. │
│ Enum+Display │ │ Box<dyn Error>│
│ Pattern match│ │ .context() │
└─────────────┘ └─────────────┘
Exemplo do Mundo Real
Um sistema de processamento de pedidos com múltiplas camadas de erros:
use std::collections::HashMap;
use std::fmt;
// ============================================================
// Camada 1: Erros de IO/Infraestrutura
// ============================================================
#[derive(Debug)]
enum ErroInfra {
ArquivoNaoEncontrado(String),
TimeoutConexao { host: String, porta: u16 },
PermissaoNegada(String),
}
impl fmt::Display for ErroInfra {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErroInfra::ArquivoNaoEncontrado(p) => write!(f, "Arquivo não encontrado: {}", p),
ErroInfra::TimeoutConexao { host, porta } => {
write!(f, "Timeout ao conectar em {}:{}", host, porta)
}
ErroInfra::PermissaoNegada(r) => write!(f, "Permissão negada: {}", r),
}
}
}
// ============================================================
// Camada 2: Erros de Negócio
// ============================================================
#[derive(Debug)]
enum ErroNegocio {
EstoqueInsuficiente { produto: String, disponivel: u32, solicitado: u32 },
LimiteCredito { cliente: String, limite: f64, valor: f64 },
ProdutoInativo(String),
}
impl fmt::Display for ErroNegocio {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErroNegocio::EstoqueInsuficiente { produto, disponivel, solicitado } => {
write!(f, "Estoque insuficiente para '{}': {} disponível, {} solicitado",
produto, disponivel, solicitado)
}
ErroNegocio::LimiteCredito { cliente, limite, valor } => {
write!(f, "Limite de crédito excedido para '{}': limite R${:.2}, valor R${:.2}",
cliente, limite, valor)
}
ErroNegocio::ProdutoInativo(p) => write!(f, "Produto inativo: {}", p),
}
}
}
// ============================================================
// Camada 3: Erro do Serviço (agrega as camadas)
// ============================================================
#[derive(Debug)]
enum ErroServico {
Infra(ErroInfra),
Negocio(ErroNegocio),
Desconhecido(String),
}
impl From<ErroInfra> for ErroServico {
fn from(e: ErroInfra) -> Self { ErroServico::Infra(e) }
}
impl From<ErroNegocio> for ErroServico {
fn from(e: ErroNegocio) -> Self { ErroServico::Negocio(e) }
}
impl fmt::Display for ErroServico {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErroServico::Infra(e) => write!(f, "[INFRA] {}", e),
ErroServico::Negocio(e) => write!(f, "[NEGÓCIO] {}", e),
ErroServico::Desconhecido(msg) => write!(f, "[DESCONHECIDO] {}", msg),
}
}
}
// ============================================================
// Serviço de Pedidos
// ============================================================
struct Estoque {
produtos: HashMap<String, (u32, f64, bool)>, // (qtd, preço, ativo)
}
impl Estoque {
fn verificar(&self, produto: &str, qtd: u32) -> Result<f64, ErroNegocio> {
match self.produtos.get(produto) {
None => Err(ErroNegocio::ProdutoInativo(produto.to_string())),
Some((_, _, false)) => Err(ErroNegocio::ProdutoInativo(produto.to_string())),
Some((disponivel, preco, true)) => {
if *disponivel < qtd {
Err(ErroNegocio::EstoqueInsuficiente {
produto: produto.to_string(),
disponivel: *disponivel,
solicitado: qtd,
})
} else {
Ok(*preco * qtd as f64)
}
}
}
}
}
fn processar_pedido(
estoque: &Estoque,
itens: &[(&str, u32)],
cliente: &str,
limite_credito: f64,
) -> Result<f64, ErroServico> {
let mut total = 0.0;
for (produto, qtd) in itens {
// ? converte ErroNegocio -> ErroServico automaticamente
let subtotal = estoque.verificar(produto, *qtd)?;
total += subtotal;
}
if total > limite_credito {
return Err(ErroNegocio::LimiteCredito {
cliente: cliente.to_string(),
limite: limite_credito,
valor: total,
})?;
}
println!("Pedido aprovado para '{}': R${:.2}", cliente, total);
Ok(total)
}
fn main() {
let mut produtos = HashMap::new();
produtos.insert("Teclado".to_string(), (10, 250.0, true));
produtos.insert("Mouse".to_string(), (5, 80.0, true));
produtos.insert("Monitor".to_string(), (0, 1500.0, true));
produtos.insert("Webcam".to_string(), (3, 200.0, false));
let estoque = Estoque { produtos };
// Pedido bem-sucedido
let itens = vec![("Teclado", 2), ("Mouse", 1)];
match processar_pedido(&estoque, &itens, "João", 1000.0) {
Ok(total) => println!("Total: R${:.2}", total),
Err(e) => println!("Erro: {}", e),
}
println!("---");
// Estoque insuficiente
let itens = vec![("Monitor", 1)];
match processar_pedido(&estoque, &itens, "Maria", 5000.0) {
Ok(total) => println!("Total: R${:.2}", total),
Err(e) => println!("Erro: {}", e),
}
// Produto inativo
let itens = vec![("Webcam", 1)];
match processar_pedido(&estoque, &itens, "Pedro", 1000.0) {
Ok(total) => println!("Total: R${:.2}", total),
Err(e) => println!("Erro: {}", e),
}
// Limite de crédito excedido
let itens = vec![("Teclado", 5)];
match processar_pedido(&estoque, &itens, "Ana", 500.0) {
Ok(total) => println!("Total: R${:.2}", total),
Err(e) => println!("Erro: {}", e),
}
}
Quando Usar
- Sempre: Em Rust, tratamento de erros com Result/Option é o padrão — não é opcional
- thiserror: Em bibliotecas onde os consumidores precisam fazer pattern match nos erros
- anyhow: Em aplicações onde você quer propagar erros com contexto
- Erros customizados: Quando os erros padrão do Rust não expressam a semântica do seu domínio
- Conversão From: Quando múltiplas fontes de erro precisam ser unificadas
Quando NÃO Usar
- Panic para bugs: Use
panic!para bugs do programador (índice fora do limite, invariante violada), não para erros esperados - unwrap em produção: Evite
.unwrap()em código de produção — use.expect("mensagem clara")no mínimo - Erros excessivamente granulares: Se cada função tem 10 variantes de erro, considere agrupar em categorias
- anyhow em bibliotecas: Bibliotecas devem usar tipos de erro tipados (thiserror), não erros dinâmicos
Variações em Rust
Combinadores avançados
fn demonstrar_combinadores() {
let numeros = vec!["1", "2", "abc", "4", "def"];
// collect() pode coletar Result<Vec<T>, E> — para no primeiro erro
let resultado: Result<Vec<i32>, _> = numeros.iter()
.map(|s| s.parse::<i32>())
.collect();
println!("Collect (para no erro): {:?}", resultado);
// filter_map para ignorar erros
let apenas_validos: Vec<i32> = numeros.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect();
println!("Apenas válidos: {:?}", apenas_validos);
// partition para separar sucessos de erros
let (sucessos, erros): (Vec<_>, Vec<_>) = numeros.iter()
.map(|s| s.parse::<i32>())
.partition(|r| r.is_ok());
println!("Sucessos: {}, Erros: {}", sucessos.len(), erros.len());
}
Erros com backtrace
// Com a feature std::backtrace (estável desde Rust 1.65)
use std::backtrace::Backtrace;
#[derive(Debug)]
struct ErroComBacktrace {
mensagem: String,
backtrace: Backtrace,
}
impl ErroComBacktrace {
fn novo(msg: &str) -> Self {
ErroComBacktrace {
mensagem: msg.to_string(),
backtrace: Backtrace::capture(),
}
}
}
impl fmt::Display for ErroComBacktrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}\n\nBacktrace:\n{}", self.mensagem, self.backtrace)
}
}
Padrões Relacionados
- Newtype: Newtypes validados retornam Result na construção
- Builder: build() retorna Result quando campos obrigatórios podem estar faltando
- RAII: Guards com Drop para cleanup em caminhos de erro
- State: Transições inválidas retornam Result com erro descritivo
Conclusão
O sistema de tratamento de erros do Rust, centrado em Result<T, E> e Option<T>, é fundamentalmente superior a exceções. Erros são valores explícitos que o compilador obriga você a tratar. O operador ? torna a propagação ergonômica, thiserror simplifica a criação de tipos de erro em bibliotecas, e anyhow oferece flexibilidade para aplicações. A combinação dessas ferramentas resulta em código que expressa claramente seus modos de falha, propaga erros de forma elegante e garante que nenhum caso de erro seja silenciosamente ignorado.