Gestione degli Errori in Rust 2026: Result, Option, thiserror e anyhow nella Pratica
Guida completa alla gestione degli errori in Rust con Result, Option, l'operatore ? e le crate thiserror e anyhow per applicazioni robuste e manutenibili.

La gestione degli errori in Rust si basa su due enum—Result<T, E> e Option<T>—che impongono una gestione esplicita di successo, fallimento e assenza a tempo di compilazione. A differenza delle eccezioni che si propagano in modo invisibile, l'approccio di Rust rende i percorsi di errore visibili nelle firme delle funzioni, eliminando intere categorie di sorprese a runtime. L'operatore ?, combinato con le crate thiserror e anyhow, semplifica questo modello esplicito senza sacrificare la chiarezza.
Option<T> è adatto per valori che possono legittimamente essere assenti (campi di configurazione, risultati di ricerca). Result<T, E> viene utilizzato quando le operazioni possono fallire con informazioni di errore significative (I/O su file, richieste di rete, parsing).
Fondamenti di Result e Option
Result<T, E> rappresenta il successo (Ok(T)) o il fallimento (Err(E)). Option<T> rappresenta un valore (Some(T)) o l'assenza (None). Entrambi sono tipi somma—il compilatore garantisce che ogni variante venga gestita.
// Demonstrates Result and Option basic patterns
fn find_user(id: u64) -> Option<String> {
// Returns None if user doesn't exist
if id == 0 {
None
} else {
Some(format!("User-{}", id))
}
}
fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
// Returns Err if parsing fails
s.parse::<u16>()
}
fn main() {
// Option handling - must address None case
match find_user(42) {
Some(name) => println!("Found: {}", name),
None => println!("User not found"),
}
// Result handling - must address Err case
match parse_port("8080") {
Ok(port) => println!("Port: {}", port),
Err(e) => println!("Invalid port: {}", e),
}
}Il compilatore rifiuta il codice che ignora questi valori di ritorno senza una gestione esplicita. Questo design intercetta a tempo di compilazione bug che in altri linguaggi si manifesterebbero come eccezioni di puntatore nullo o errori non catturati.
L'Operatore Punto Interrogativo per una Propagazione Concisa
L'operatore ? trasforma catene di match verbose in codice lineare leggibile. Quando applicato a un Result, restituisce anticipatamente l'errore se presente, oppure estrae il valore di successo. Lo stesso vale per Option.
// Using ? for clean error propagation
use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn read_first_line(path: &str) -> Result<String, io::Error> {
let file = File::open(path)?; // Returns early if open fails
let mut reader = BufReader::new(file);
let mut line = String::new();
reader.read_line(&mut line)?; // Returns early if read fails
Ok(line.trim().to_string())
}
fn get_port_from_config(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
let content = read_first_line(path)?;
let port = content.parse::<u16>()?; // ParseIntError converts via From
Ok(port)
}L'operatore ? richiede che il tipo di errore sia convertibile nel tipo di errore di ritorno della funzione tramite il trait From. L'utilizzo di Box<dyn std::error::Error> come mostrato sopra accetta qualsiasi tipo di errore che implementa il trait Error standard.
Creazione di Errori Personalizzati con thiserror
La crate thiserror elimina il boilerplate per i tipi di errore personalizzati. Deriva le implementazioni di Error, Display e From attraverso una macro procedurale.
// Custom error types using thiserror 2.0
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("configuration file not found at {path}")]
NotFound { path: String },
#[error("invalid port number: {0}")]
InvalidPort(#[from] std::num::ParseIntError),
#[error("IO error reading config")]
IoError(#[from] std::io::Error),
#[error("missing required field: {0}")]
MissingField(String),
}
fn load_config(path: &str) -> Result<Config, ConfigError> {
let content = std::fs::read_to_string(path)?; // IoError auto-converts
let port: u16 = content
.lines()
.find(|l| l.starts_with("port="))
.ok_or(ConfigError::MissingField("port".into()))?
.strip_prefix("port=")
.unwrap()
.parse()?; // ParseIntError auto-converts to InvalidPort
Ok(Config { port })
}
struct Config {
port: u16,
}L'attributo #[from] genera conversioni automatiche, consentendo un uso fluido di ? con diversi tipi di errore sottostanti. I messaggi di errore diventano autodocumentanti attraverso le stringhe di formato #[error(...)].
Errori a Livello di Applicazione con anyhow
Mentre thiserror è adatto al codice di libreria con tipi di errore specifici, anyhow è orientato alle applicazioni dove il contesto dell'errore è più importante della granularità del tipo. Il trait Context aggiunge messaggi descrittivi a qualsiasi errore.
// Application error handling with anyhow 1.0
use anyhow::{Context, Result, bail, ensure};
fn load_database_url() -> Result<String> {
std::env::var("DATABASE_URL")
.context("DATABASE_URL environment variable not set")
}
fn connect_to_database(url: &str) -> Result<DatabaseConnection> {
ensure!(!url.is_empty(), "database URL cannot be empty");
let conn = DatabaseConnection::new(url)
.context("failed to establish database connection")?;
if !conn.is_healthy() {
bail!("database connection unhealthy after establishment");
}
Ok(conn)
}
fn main() -> Result<()> {
let url = load_database_url()?;
let conn = connect_to_database(&url)
.context("application startup failed")?;
// Context chains create readable error traces:
// Error: application startup failed
// Caused by:
// 0: failed to establish database connection
// 1: connection refused
Ok(())
}
struct DatabaseConnection;
impl DatabaseConnection {
fn new(_url: &str) -> Result<Self> { Ok(Self) }
fn is_healthy(&self) -> bool { true }
}Il metodo context() avvolge gli errori con informazioni aggiuntive, creando una catena che aiuta nel debugging. La macro bail! fornisce un'uscita anticipata con un messaggio di errore formattato, mentre ensure! agisce come un'asserzione che restituisce un errore invece di andare in panic.
Pronto a superare i tuoi colloqui su Rust?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Combinare thiserror e anyhow in Progetti Reali
Le librerie espongono errori strutturati tramite thiserror per la gestione programmatica da parte dei consumatori. Le applicazioni avvolgono questi errori con anyhow per un output leggibile. Questa separazione mantiene le API pulite preservando la debuggabilità.
// Exposes typed errors for programmatic handling
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PaymentError {
#[error("insufficient funds: required {required}, available {available}")]
InsufficientFunds { required: u64, available: u64 },
#[error("card declined: {reason}")]
CardDeclined { reason: String },
#[error("payment provider unavailable")]
ProviderUnavailable(#[source] reqwest::Error),
}
pub fn process_payment(amount: u64) -> Result<Receipt, PaymentError> {
// Library returns specific, matchable error types
Err(PaymentError::InsufficientFunds {
required: amount,
available: 50,
})
}
pub struct Receipt;// Wraps library errors with context
use anyhow::{Context, Result};
use my_payment_lib::{process_payment, PaymentError};
fn checkout(cart_total: u64) -> Result<()> {
match process_payment(cart_total) {
Ok(_receipt) => Ok(()),
Err(PaymentError::InsufficientFunds { required, available }) => {
// Handle specific case differently
println!("Add {} to your balance", required - available);
Ok(())
}
Err(e) => Err(e).context("checkout payment processing failed"),
}
}Questo pattern consente ai chiamanti di fare match su varianti specifiche quando il recupero è possibile, beneficiando comunque di un ricco contesto di errore quando si propagano i fallimenti verso l'alto. La comunità Rust si è largamente standardizzata su questo approccio, come discusso nelle linee guida API di Rust.
Pattern di Gestione Errori per Codice Asincrono
Le funzioni async restituiscono Result esattamente come quelle sincrone. L'operatore ? funziona in modo identico all'interno dei blocchi async, e sia thiserror che anyhow si integrano senza modifiche.
// Error handling in async Rust with Tokio
use anyhow::{Context, Result};
use std::time::Duration;
async fn fetch_user_data(user_id: u64) -> Result<UserData> {
let response = reqwest::get(format!("https://api.example.com/users/{}", user_id))
.await
.context("HTTP request to user API failed")?;
let status = response.status();
if !status.is_success() {
anyhow::bail!("user API returned status {}", status);
}
let data: UserData = response
.json()
.await
.context("failed to parse user data JSON")?;
Ok(data)
}
async fn fetch_with_retry(user_id: u64, attempts: u32) -> Result<UserData> {
let mut last_error = None;
for attempt in 1..=attempts {
match fetch_user_data(user_id).await {
Ok(data) => return Ok(data),
Err(e) => {
last_error = Some(e);
if attempt < attempts {
tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await;
}
}
}
}
Err(last_error.unwrap()).context(format!("failed after {} attempts", attempts))
}
#[derive(serde::Deserialize)]
struct UserData {
name: String,
}Quando si combinano più operazioni asincrone, si consiglia try_join! da tokio o futures per eseguirle contemporaneamente propagando il primo errore. Per concetti correlati, il modulo domande sul colloquio async/await offre ulteriori approfondimenti.
Downcasting e Ispezione degli Errori
Sia anyhow::Error che Box<dyn Error> supportano il downcasting per recuperare il tipo di errore originale. Questo consente di loggare dettagli specifici continuando a propagare errori generici.
// Inspecting wrapped error types
use anyhow::{Context, Result};
use std::io;
fn log_and_propagate(result: Result<()>) -> Result<()> {
if let Err(ref e) = result {
// Check if the root cause is a specific type
if let Some(io_err) = e.downcast_ref::<io::Error>() {
match io_err.kind() {
io::ErrorKind::NotFound => {
tracing::warn!("file not found, using defaults");
}
io::ErrorKind::PermissionDenied => {
tracing::error!("permission denied - check file ownership");
}
_ => {
tracing::error!("IO error: {:?}", io_err);
}
}
}
}
result
}Il downcasting colma il divario tra gestione generica degli errori e logica di recupero specifica. Dovrebbe essere usato con parsimonia—se si verifica frequente downcasting, considerare se un enum di errore tipizzato sarebbe più appropriato.
Conversione tra Option e Result
La libreria standard fornisce metodi per convertire tra Option e Result, consentendo una composizione fluida quando diverse API utilizzano pattern differenti.
// Option and Result interoperability
fn get_env_port() -> Option<u16> {
std::env::var("PORT")
.ok() // Result -> Option (discards error)
.and_then(|s| s.parse().ok())
}
fn get_env_port_with_error() -> Result<u16, String> {
std::env::var("PORT")
.map_err(|_| "PORT not set".to_string())?
.parse()
.map_err(|_| "PORT is not a valid number".to_string())
}
fn lookup_and_parse(map: &std::collections::HashMap<String, String>, key: &str) -> Result<u16, String> {
map.get(key)
.ok_or_else(|| format!("key '{}' not found", key))? // Option -> Result
.parse()
.map_err(|e| format!("parse error for '{}': {}", key, e))
}Il metodo ok() scarta i dettagli dell'errore quando conta solo la presenza. I metodi ok_or() e ok_or_else() convertono None in un errore personalizzato, consentendo la propagazione con ? dai valori Option. Questi pattern appaiono frequentemente nelle domande sul colloquio sul pattern matching.
Considerazioni sulle Prestazioni
La gestione degli errori in Rust non comporta costi a runtime sul percorso di successo. Result e Option sono enum allocati sullo stack con dimensione deterministica. Il compilatore ottimizza i controlli quando può dimostrare che un ramo è irraggiungibile.
| Approccio | Costo Percorso Successo | Costo Percorso Errore | |-----------|------------------------|----------------------| | Result/Option | Zero | Stack unwinding (economico) | | panic! | Zero | Stack unwinding completo + cleanup | | Eccezioni C++ | Zero (solitamente) | Costosa allocazione heap + RTTI |
unwrap() e expect() dovrebbero essere evitati nel codice di libreria—sono riservati ai casi in cui un fallimento indica genuinamente un bug. Per percorsi critici per le prestazioni dove gli errori sono comuni, considerare l'uso di enum con dati inline piuttosto che tipi di errore allocati sull'heap.
Conclusione
Result<T, E>gestisce i fallimenti recuperabili;Option<T>gestisce l'assenza—entrambi impongono la gestione a tempo di compilazione- L'operatore
?propaga gli errori in modo conciso, richiedendo implementazioni del traitFromper la conversione dei tipi thiserrorgenera tipi di errore strutturati per le librerie senza boilerplateanyhowfornisce catene di errori contestuali per le applicazioni, supportandocontext(),bail!eensure!- Combinare entrambi: le librerie espongono errori tipizzati tramite
thiserror, le applicazioni li avvolgono conanyhow - Il codice asincrono usa pattern identici—
?funziona nei blocchi async senza modifiche ok_or()converteOptioninResult;ok()per il contrario- Fare downcasting degli errori avvolti solo quando la logica di recupero specifica richiede il tipo originale
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Condividi
Articoli correlati

Rust smart pointer spiegati: Box, Rc, Arc e RefCell nel 2026
Gli smart pointer di Rust Box, Rc, Arc e RefCell spiegati con esempi compilabili del 2026, una tabella decisionale e le domande da colloquio più comuni.

Rust 2026: Traits, Generics e Domande Avanzate da Colloquio – Guida Completa
Guida approfondita a traits e generics in Rust con le novità della 2024 Edition: trait upcasting, AsyncFn, RPITIT e pattern avanzati per colloqui tecnici.

Rust per il Web: Actix Web vs Axum – Confronto e domande da colloquio 2026
Un confronto pratico tra Actix Web 4.13 e Axum 0.8 per lo sviluppo web in Rust nel 2026. Architettura, prestazioni, esperienza di sviluppo e domande da colloquio per posizioni backend Rust.