Salta ai contenuti

Errori personalizzati in Rust

Nella pagina su Result, ogni errore era semplicemente una String. Per piccoli esempi va benissimo, ma non regge più quando una funzione può fallire in più di un modo distinto: su una String non puoi fare match, quindi chi chiama non può sapere quale tipo di fallimento è avvenuto senza analizzare il testo.

fn withdraw(balance: f64, amount: f64) -> Result<f64, String> {
if amount > balance {
return Err(String::from("fondi insufficienti"));
}
if amount < 0.0 {
return Err(String::from("l'importo non può essere negativo"));
}
Ok(balance - amount)
}

Se chi chiama vuole reagire in modo diverso a «fondi insufficienti» e a «importo negativo», l’unica possibilità è confrontare il testo dell’errore: una soluzione fragile, che si rompe appena riformuli un messaggio.

Un’enum risolve il problema: ogni tipo di fallimento diventa una variante a sé, su cui chi chiama può fare direttamente match:

enum WithdrawError {
InsufficientFunds,
NegativeAmount,
}
fn withdraw(balance: f64, amount: f64) -> Result<f64, WithdrawError> {
if amount < 0.0 {
return Err(WithdrawError::NegativeAmount);
}
if amount > balance {
return Err(WithdrawError::InsufficientFunds);
}
Ok(balance - amount)
}
fn main() {
match withdraw(100.0, 150.0) {
Ok(remaining) => println!("Nuovo saldo: {remaining}"),
Err(WithdrawError::InsufficientFunds) => println!("Non ci sono abbastanza soldi"),
Err(WithdrawError::NegativeAmount) => println!("Non puoi prelevare un importo negativo"),
}
}

Output:

Non ci sono abbastanza soldi
▶ Provalo tu

Ora è il compilatore a garantire che ogni tipo di fallimento venga gestito: è la stessa garanzia che match ti ha sempre dato con le enum, solo applicata agli errori.

Implementare Display per un messaggio di errore come si deve

Sezione intitolata “Implementare Display per un messaggio di errore come si deve”

Prova adesso a stampare WithdrawError con {}: non compila, perché Rust non sa come trasformarlo in testo. Implementare il trait Display risolve il problema. I trait avranno presto pagine tutte loro (Trait e Trait comuni). Per ora puoi copiare questo schema così com’è:

use std::fmt;
enum WithdrawError {
InsufficientFunds,
NegativeAmount,
}
impl fmt::Display for WithdrawError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
WithdrawError::InsufficientFunds => write!(f, "fondi insufficienti"),
WithdrawError::NegativeAmount => write!(f, "l'importo non può essere negativo"),
}
}
}
fn main() {
let error = WithdrawError::InsufficientFunds;
println!("Errore: {error}");
}

Output:

Errore: fondi insufficienti
▶ Provalo tu

Ora WithdrawError si comporta come qualsiasi altro errore che vorresti mostrare a un utente o scrivere in un log, e resta comunque possibile farci match nel codice che deve reagire a un caso preciso.

Scrivere a mano Display e tutto il codice ripetitivo attorno a un tipo di errore personalizzato diventa noioso in un progetto vero. Il crate thiserror (in inglese) ne genera quasi tutto con un #[derive], e anyhow (in inglese) è molto usato nel codice applicativo che deve solo propagare gli errori senza definire un tipo per ognuno. Vale la pena sapere che esistono, ma non ti servono per imparare le basi. Quando sei pronto, la pagina anyhow e thiserror li mostra entrambi in azione e spiega quando scegliere l’uno o l’altro.

Definisci un’enum LoginError con due varianti, EmptyUsername e PasswordTooShort, e implementa Display per lei. Poi scrivi fn check_login(user: &str, password: &str) -> Result<String, LoginError>, che restituisce Ok con un messaggio di benvenuto, oppure l’errore giusto. Una password deve avere almeno 8 caratteri. Suggerimento: .is_empty() e .len() su un &str ti danno tutto quello che serve per i controlli.

Mostra la soluzione
use std::fmt;
enum LoginError {
EmptyUsername,
PasswordTooShort,
}
impl fmt::Display for LoginError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LoginError::EmptyUsername => write!(f, "il nome utente non può essere vuoto"),
LoginError::PasswordTooShort => write!(f, "la password deve avere almeno 8 caratteri"),
}
}
}
fn check_login(user: &str, password: &str) -> Result<String, LoginError> {
if user.is_empty() {
return Err(LoginError::EmptyUsername);
}
if password.len() < 8 {
return Err(LoginError::PasswordTooShort);
}
Ok(format!("Benvenuta, {user}!"))
}
fn main() {
let attempts = [("ana", "correct-horse"), ("", "secret123"), ("ben", "short")];
for (user, password) in attempts {
match check_login(user, password) {
Ok(message) => println!("{message}"),
Err(e) => println!("Errore: {e}"),
}
}
}

Output:

Benvenuta, ana!
Errore: il nome utente non può essere vuoto
Errore: la password deve avere almeno 8 caratteri
▶ Provalo tu

Le password di prova restano in inglese e senza accenti: .len() conta i byte, e una lettera accentata ne occupa due.

Scrivi fn parse_percent(input: &str) -> Result<u32, PercentError>, dove PercentError ha due varianti che contengono dati: NotANumber(String) (il testo sbagliato) e TooLarge(u32) (il numero che superava 100). Fai in modo che i messaggi di Display includano questi dettagli. Suggerimento: le varianti di un’enum possono contenere valori, come nella pagina sulle enum. In fmt, fai match con PercentError::TooLarge(n) => ... per estrarre il valore.

Mostra la soluzione
use std::fmt;
enum PercentError {
NotANumber(String),
TooLarge(u32),
}
impl fmt::Display for PercentError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PercentError::NotANumber(text) => write!(f, "'{text}' non è un numero"),
PercentError::TooLarge(n) => write!(f, "{n} è più di 100"),
}
}
}
fn parse_percent(input: &str) -> Result<u32, PercentError> {
match input.parse::<u32>() {
Ok(n) => {
if n > 100 {
Err(PercentError::TooLarge(n))
} else {
Ok(n)
}
}
Err(_) => Err(PercentError::NotANumber(input.to_string())),
}
}
fn main() {
for input in ["75", "metà", "250"] {
match parse_percent(input) {
Ok(n) => println!("{n}%"),
Err(e) => println!("Errore: {e}"),
}
}
}

Output:

75%
Errore: 'metà' non è un numero
Errore: 250 è più di 100
▶ Provalo tu
  • Un errore String va bene per piccoli esempi, ma chi chiama non può distinguere i casi con match.
  • Definisci un’enum con una variante per ogni tipo di fallimento, così chi chiama può fare match sull’errore preciso.
  • Implementa Display (impl fmt::Display for TuoErrore) per poter stampare il tipo di errore con {}.
  • Nei progetti più grandi, crate come thiserror e anyhow eliminano quasi tutto il codice ripetitivo.

Verifica veloce

1. Perché un tipo di errore String non basta più quando una funzione può fallire in più modi?

2. Cosa permette a chi chiama withdraw di reagire in modo diverso a InsufficientFunds e a NegativeAmount?

3. Cosa ti permette di fare l'implementazione di Display per un tipo di errore personalizzato?

4. In cosa aiutano crate come thiserror e anyhow?

5. Perché confrontare il testo grezzo dei messaggi di errore è considerato fragile?

Punteggio: 0 / 5