Gestione degli Errori in Rust
La gestione degli errori e uno degli aspetti piu distintivi di Rust. Il linguaggio distingue chiaramente tra errori irrecuperabili (gestiti con panic!) ed errori recuperabili (gestiti con Result<T, E>), forzando il programmatore a trattare ogni possibile fallimento in modo esplicito.
panic! per Errori Irrecuperabili
La macro panic! termina il programma con un messaggio di errore. Si usa per situazioni da cui non e possibile riprendersi, come bug logici o violazioni di invarianti.
fn main() {
// Panic esplicito
// panic!("Qualcosa e andato terribilmente storto!");
// Panic implicito: accesso fuori dai limiti
let v = vec![1, 2, 3];
// v[10]; // PANIC: index out of bounds
// Panic con formattazione
let versione = 2;
if versione < 3 {
// panic!("Versione {} non supportata, richiesta >= 3", versione);
println!("Versione {} rilevata (esempio senza panic)", versione);
}
}
Per visualizzare il backtrace durante un panic, si imposta la variabile d’ambiente RUST_BACKTRACE=1.
Result per Errori Recuperabili
La maggior parte degli errori in Rust sono recuperabili e si gestiscono con Result<T, E>:
use std::fs::File;
use std::io::{self, Read};
fn leggi_file(percorso: &str) -> Result<String, io::Error> {
let mut file = File::open(percorso)?;
let mut contenuto = String::new();
file.read_to_string(&mut contenuto)?;
Ok(contenuto)
}
fn main() {
match leggi_file("dati.txt") {
Ok(contenuto) => println!("Contenuto:\n{}", contenuto),
Err(errore) => match errore.kind() {
io::ErrorKind::NotFound => eprintln!("File non trovato"),
io::ErrorKind::PermissionDenied => eprintln!("Permesso negato"),
_ => eprintln!("Errore: {}", errore),
},
}
}
Tipi di Errore Custom
Per applicazioni complesse, e buona pratica definire i propri tipi di errore:
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum ErroreApp {
ConfigNonTrovata(String),
PortaNonValida(ParseIntError),
ConnessioneFallita { host: String, porta: u16 },
}
impl fmt::Display for ErroreApp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ErroreApp::ConfigNonTrovata(percorso) => {
write!(f, "File di configurazione non trovato: {}", percorso)
}
ErroreApp::PortaNonValida(e) => {
write!(f, "Porta non valida: {}", e)
}
ErroreApp::ConnessioneFallita { host, porta } => {
write!(f, "Impossibile connettersi a {}:{}", host, porta)
}
}
}
}
impl std::error::Error for ErroreApp {}
Conversione degli Errori con From
Il trait From permette la conversione automatica tra tipi di errore, abilitando l’uso dell’operatore ?:
use std::num::ParseIntError;
use std::io;
use std::fmt;
#[derive(Debug)]
enum ErroreConfig {
Io(io::Error),
Parsing(ParseIntError),
Validazione(String),
}
impl fmt::Display for ErroreConfig {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ErroreConfig::Io(e) => write!(f, "Errore I/O: {}", e),
ErroreConfig::Parsing(e) => write!(f, "Errore parsing: {}", e),
ErroreConfig::Validazione(msg) => write!(f, "Validazione: {}", msg),
}
}
}
impl std::error::Error for ErroreConfig {}
// Conversioni automatiche con From
impl From<io::Error> for ErroreConfig {
fn from(e: io::Error) -> Self {
ErroreConfig::Io(e)
}
}
impl From<ParseIntError> for ErroreConfig {
fn from(e: ParseIntError) -> Self {
ErroreConfig::Parsing(e)
}
}
fn carica_config(percorso: &str) -> Result<u16, ErroreConfig> {
let contenuto = std::fs::read_to_string(percorso)?; // io::Error -> ErroreConfig
let porta: u16 = contenuto.trim().parse()?; // ParseIntError -> ErroreConfig
if porta < 1024 {
return Err(ErroreConfig::Validazione(
format!("Porta {} troppo bassa, usare >= 1024", porta),
));
}
Ok(porta)
}
Propagazione con l’Operatore ?
L’operatore ? semplifica enormemente la propagazione degli errori:
use std::fs;
use std::io;
// Senza l'operatore ?
fn leggi_senza_operatore(path: &str) -> Result<String, io::Error> {
let risultato = fs::read_to_string(path);
match risultato {
Ok(s) => Ok(s.trim().to_uppercase()),
Err(e) => Err(e),
}
}
// Con l'operatore ?
fn leggi_con_operatore(path: &str) -> Result<String, io::Error> {
let contenuto = fs::read_to_string(path)?;
Ok(contenuto.trim().to_uppercase())
}
// Concatenazione con ?
fn elabora_dati(path: &str) -> Result<Vec<i32>, Box<dyn std::error::Error>> {
let contenuto = fs::read_to_string(path)?;
let numeri: Result<Vec<i32>, _> = contenuto
.lines()
.map(|riga| riga.trim().parse::<i32>())
.collect();
Ok(numeri?)
}
La Crate thiserror
thiserror semplifica la creazione di tipi di errore custom con derive macro:
// In Cargo.toml: thiserror = "2"
// use thiserror::Error;
//
// #[derive(Error, Debug)]
// enum ErroreDatabase {
// #[error("Connessione fallita: {0}")]
// Connessione(String),
//
// #[error("Query non valida: {query}")]
// QueryNonValida { query: String },
//
// #[error("Record non trovato con id {id}")]
// NonTrovato { id: u64 },
//
// #[error(transparent)]
// Io(#[from] std::io::Error),
//
// #[error(transparent)]
// Parsing(#[from] std::num::ParseIntError),
// }
thiserror genera automaticamente le implementazioni di Display, Error e From, riducendo notevolmente il codice ripetitivo.
La Crate anyhow
anyhow e pensata per applicazioni (non librerie) dove non serve un tipo di errore strutturato:
// In Cargo.toml: anyhow = "1"
// use anyhow::{Context, Result};
//
// fn carica_e_valida(percorso: &str) -> Result<Config> {
// let contenuto = std::fs::read_to_string(percorso)
// .context(format!("Impossibile leggere {}", percorso))?;
//
// let config: Config = serde_json::from_str(&contenuto)
// .context("JSON non valido nel file di configurazione")?;
//
// if config.porta == 0 {
// anyhow::bail!("La porta non puo essere 0");
// }
//
// Ok(config)
// }
//
// fn main() -> Result<()> {
// let config = carica_e_valida("config.json")?;
// println!("Config caricata: {:?}", config);
// Ok(())
// }
La differenza chiave: usa thiserror per librerie (errori tipizzati) e anyhow per applicazioni (errori generici con contesto).
Best Practices
Ecco le linee guida per una gestione errori efficace in Rust:
use std::io;
// 1. Usa panic! solo per bug e violazioni di invarianti
fn elemento_sicuro(v: &[i32], indice: usize) -> i32 {
assert!(!v.is_empty(), "Il vettore non puo essere vuoto");
v[indice] // panic se fuori range: e un bug del chiamante
}
// 2. Usa Result per tutto cio che puo legittimamente fallire
fn analizza_porta(input: &str) -> Result<u16, String> {
let porta: u16 = input.parse().map_err(|e| format!("Parsing: {}", e))?;
if porta == 0 {
Err("La porta 0 non e valida".to_string())
} else {
Ok(porta)
}
}
// 3. Usa ? per propagare, match per gestire
fn configura_server() -> Result<(), Box<dyn std::error::Error>> {
let porta = analizza_porta("8080")?; // Propaga l'errore
println!("Server sulla porta {}", porta);
Ok(())
}
// 4. main puo restituire Result
fn main() -> Result<(), Box<dyn std::error::Error>> {
configura_server()?;
Ok(())
}
Conclusione
La gestione degli errori in Rust e progettata per essere esplicita, componibile e sicura. panic! gestisce i casi irrecuperabili, Result quelli recuperabili, e l’operatore ? rende la propagazione elegante. Per progetti reali, thiserror e anyhow sono strumenti essenziali: il primo per definire errori tipizzati nelle librerie, il secondo per gestire errori con contesto nelle applicazioni. Seguendo queste pratiche, il codice Rust risulta robusto e manutenibile.