Sobre nós Guias Projetos Contactos
Админка
please wait

Introdução

O tratamento de erros em Rust é uma das suas funcionalidades mais distintivas. Ao contrário das exceções noutras linguagens, Rust torna os erros explícitos através do sistema de tipos, obrigando os programadores a lidar com casos de falha. Esta abordagem conduz a software mais robusto, mas requer compreender os tipos Result, a conversão de erros e o ecossistema de crates de tratamento de erros.

O Tipo Result

Utilização Básica de Result

fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Cannot divide by zero".to_string())
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
// Utilizar if let para casos simples
if let Ok(result) = divide(10.0, 0.0) {
println!("Result: {}", result);
} else {
println!("Division failed");
}
}

O Operador ?

fn read_config() -> Result<Config, std::io::Error> {
let contents = std::fs::read_to_string("config.json")?; // Devolve mais cedo em caso de erro
let config: Config = serde_json::from_str(&contents)?; // Requer implementação de From
Ok(config)
}

unwrap e expect

// unwrap: panic em caso de erro (evitar em produção)
let file = std::fs::read_to_string("config.json").unwrap();
// expect: panic com mensagem
let file = std::fs::read_to_string("config.json")
.expect("Failed to read config file");
// Utilizar apenas quando:
// - Sabe que o erro é impossível
// - O erro indicaria um bug
// - Em exemplos/protótipos

Tipos de Erro Personalizados

Erros Simples com Enum

#[derive(Debug)]
pub enum AppError {
NotFound,
PermissionDenied,
InvalidInput(String),
DatabaseError(String),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::NotFound => write!(f, "Resource not found"),
AppError::PermissionDenied => write!(f, "Permission denied"),
AppError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
AppError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
}
}
}
impl std::error::Error for AppError {}

Utilizar thiserror

A crate thiserror simplifica definições de erros personalizados:

use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Permission denied")]
PermissionDenied,
#[error("Invalid input: {message}")]
InvalidInput { message: String },
#[error("Database error")]
Database(#[from] sqlx::Error),
#[error("IO error")]
Io(#[from] std::io::Error),
#[error("Configuration error: {0}")]
Config(#[from] config::ConfigError),
}
// Agora pode usar ? com conversão automática
fn get_user(id: i32) -> Result<User, AppError> {
let user = db.query("SELECT * FROM users WHERE id = ?", id)?; // sqlx::Error -> AppError
user.ok_or_else(|| AppError::NotFound(format!("User {}", id)))
}

Utilizar anyhow em Aplicações

Para código de aplicação em que não precisa de tipos de erro específicos:

use anyhow::{Context, Result, bail, ensure};
fn process_file(path: &str) -> Result<Data> {
let contents = std::fs::read_to_string(path)
.context("Failed to read input file")?;
let data: Data = serde_json::from_str(&contents)
.context("Failed to parse JSON")?;
// Erro condicional
ensure!(!data.items.is_empty(), "Data must have at least one item");
// Devolução antecipada com erro
if data.version < 2 {
bail!("Unsupported data version: {}", data.version);
}
Ok(data)
}
fn main() {
match process_file("data.json") {
Ok(data) => println!("Processed: {:?}", data),
Err(e) => {
// Imprimir erro com cadeia
eprintln!("Error: {:?}", e);
// Ou apenas a mensagem
eprintln!("Error: {}", e);
}
}
}

Conversão de Erros

Implementação do Trait From

use std::num::ParseIntError;
#[derive(Debug)]
enum MyError {
Parse(ParseIntError),
Negative,
}
impl From<ParseIntError> for MyError {
fn from(err: ParseIntError) -> Self {
MyError::Parse(err)
}
}
fn parse_positive(s: &str) -> Result<u32, MyError> {
let num: i32 = s.parse()?; // ParseIntError convertido para MyError
if num < 0 {
Err(MyError::Negative)
} else {
Ok(num as u32)
}
}

Converter Entre Tipos de Erro

// Mapear o tipo de erro
let result = some_operation()
.map_err(|e| AppError::Operation(e.to_string()))?;
// Com contexto usando anyhow
let result = some_operation()
.map_err(|e| anyhow::anyhow!("Operation failed: {}", e))?;

Option vs Result

// Option: O valor pode não existir (a ausência não é um erro)
fn find_user(id: i32) -> Option<User> {
users.iter().find(|u| u.id == id).cloned()
}
// Result: A operação pode falhar (a ausência é um erro)
fn get_user(id: i32) -> Result<User, AppError> {
users.iter()
.find(|u| u.id == id)
.cloned()
.ok_or(AppError::NotFound)
}
// Converter Option para Result
let user = find_user(123).ok_or(AppError::NotFound)?;

Lidar com Vários Tipos de Erro

Box<dyn Error>

use std::error::Error;
fn process() -> Result<(), Box<dyn Error>> {
let file = std::fs::read_to_string("config.json")?;
let num: i32 = file.trim().parse()?;
println!("Number: {}", num);
Ok(())
}

Erro Personalizado com Source

use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Failed to read configuration")]
ConfigRead(#[source] std::io::Error),
#[error("Failed to parse configuration")]
ConfigParse {
#[source]
source: serde_json::Error,
path: String,
},
}
// Aceder à source
fn handle_error(e: AppError) {
eprintln!("Error: {}", e);
if let Some(source) = e.source() {
eprintln!("Caused by: {}", source);
}
}

Padrões de Tratamento de Erros

Combinadores de Result

// map: Transformar o valor Ok
let result = "42".parse::<i32>().map(|n| n * 2);
// map_err: Transformar o valor Err
let result = operation().map_err(|e| AppError::from(e));
// and_then: Encadear operações
let result = "42".parse::<i32>()
.and_then(|n| {
if n > 0 { Ok(n) }
else { Err("must be positive".parse().unwrap()) }
});
// or_else: Lidar com o erro e possivelmente recuperar
let result = operation()
.or_else(|_| fallback_operation());
// unwrap_or: Valor por omissão em caso de erro
let value = "invalid".parse::<i32>().unwrap_or(0);
// unwrap_or_else: Valor por omissão calculado
let value = "invalid".parse::<i32>()
.unwrap_or_else(|_| calculate_default());

Recolher Results

// Parar no primeiro erro
let results: Result<Vec<i32>, _> = strings
.iter()
.map(|s| s.parse::<i32>())
.collect();
// Recolher sucessos, ignorar erros
let values: Vec<i32> = strings
.iter()
.filter_map(|s| s.parse().ok())
.collect();
// Separar em sucessos e erros
let (successes, errors): (Vec<_>, Vec<_>) = strings
.iter()
.map(|s| s.parse::<i32>())
.partition(Result::is_ok);
let values: Vec<i32> = successes.into_iter().map(Result::unwrap).collect();

Tratamento de Erros em Async

use anyhow::Result;
async fn fetch_data(url: &str) -> Result<Data> {
let response = reqwest::get(url)
.await
.context("Failed to fetch URL")?;
let data = response
.json::<Data>()
.await
.context("Failed to parse response")?;
Ok(data)
}
#[tokio::main]
async fn main() -> Result<()> {
let data = fetch_data("https://api.example.com/data").await?;
println!("{:?}", data);
Ok(())
}

Registo de Erros

use tracing::{error, warn, info};
fn process_request(req: Request) -> Result<Response, AppError> {
let user = get_user(req.user_id).map_err(|e| {
error!(user_id = req.user_id, error = ?e, "Failed to get user");
e
})?;
// Ou com anyhow e tracing
let data = fetch_data(&req.url)
.inspect_err(|e| error!(url = %req.url, error = ?e, "Fetch failed"))?;
Ok(Response::new(data))
}

Boas Práticas

Erros em Bibliotecas vs Aplicações

Bibliotecas: Use tipos de erro específicos com thiserror.

// Bom para bibliotecas — os chamadores podem fazer match nas variantes
#[derive(Error, Debug)]
pub enum ParseError {
#[error("Invalid format at position {0}")]
InvalidFormat(usize),
#[error("Unexpected end of input")]
UnexpectedEof,
}

Aplicações: Use anyhow por conveniência.

// Bom para aplicações — fácil adicionar contexto
fn main() -> anyhow::Result<()> {
let config = load_config().context("Failed to load configuration")?;
run_server(config).context("Server error")?;
Ok(())
}

Não Fazer Panic em Bibliotecas

// Mau: faz panic com input inválido
pub fn process(data: &str) -> Output {
let parsed = data.parse().unwrap(); // Não faça isto
// ...
}
// Bom: devolver Result
pub fn process(data: &str) -> Result<Output, ParseError> {
let parsed = data.parse()?;
// ...
}

Adicionar Contexto aos Erros

// Mau: apenas propagar
let file = std::fs::read_to_string(path)?;
// Bom: adicionar contexto
let file = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path))?;

Gestão Avançada de Memória e Erros

Compreender como os erros interagem com o modelo de ownership de Rust é crucial para engenheiros sénior.

Lifetimes em Tipos de Erro

Quando os tipos de erro mantêm referências, tem de anotar lifetimes:

#[derive(Error, Debug)]
pub enum ParseError<'a> {
#[error("Invalid token at: {0}")]
InvalidToken(&'a str), // Reference to input string
#[error("Unexpected end of input")]
UnexpectedEof,
}
fn parse<'a>(input: &'a str) -> Result<Data, ParseError<'a>> {
// O erro devolvido pode referenciar o input
if input.is_empty() {
return Err(ParseError::UnexpectedEof);
}
// ...
}

Mutabilidade Interior com Erros

Por vezes, é necessário acumular erros mantendo uma referência imutável:

use std::cell::RefCell;
struct Validator {
errors: RefCell<Vec<String>>,
}
impl Validator {
fn validate(&self, data: &Data) {
// Temos &self (imutável), mas podemos adicionar erros
if data.name.is_empty() {
self.errors.borrow_mut().push("Name is required".to_string());
}
}
fn into_result(self) -> Result<(), Vec<String>> {
let errors = self.errors.into_inner();
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}

Smart Pointers no Tratamento de Erros

Para erros que precisam de ser partilhados entre threads:

use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct ErrorCollector {
errors: Arc<Mutex<Vec<AppError>>>,
}
impl ErrorCollector {
fn add_error(&self, error: AppError) {
let mut errors = self.errors.lock().unwrap();
errors.push(error);
}
fn has_errors(&self) -> bool {
let errors = self.errors.lock().unwrap();
!errors.is_empty()
}
}
// Seguro para partilhar entre threads
async fn process_batch(collector: ErrorCollector, items: Vec<Item>) {
let handles: Vec<_> = items.into_iter().map(|item| {
let collector = collector.clone();
tokio::spawn(async move {
if let Err(e) = process_item(&item).await {
collector.add_error(e);
}
})
}).collect();
futures::future::join_all(handles).await;
}

O Borrow Checker e a Propagação de Erros

Um erro comum é tentar devolver referências a dados locais em erros:

// MAU: não compila — devolver referência para um local
fn validate_bad(input: String) -> Result<Data, &str> {
if input.is_empty() {
return Err("Input is empty"); // Este &str vive mais do que o input
}
// ...
}
// BOM: ser dono dos dados do erro
fn validate_good(input: String) -> Result<Data, String> {
if input.is_empty() {
return Err("Input is empty".to_string()); // String detida
}
// ...
}

Conclusão

O tratamento de erros em Rust impõe o tratamento explícito de casos de falha, conduzindo a código mais robusto. Use thiserror para tipos de erro de bibliotecas e anyhow para código de aplicação. Adicione sempre contexto aos erros e prefira Result a panics. Compreender como os erros interagem com lifetimes, ownership e smart pointers é essencial para escrever código Rust de nível sénior. Os padrões neste guia ajudam-no a escrever código de tratamento de erros que é simultaneamente seguro e ergonómico.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive