Les erreurs personnalisées en Rust
Sur la page Result, chaque erreur n’était qu’une simple String. Cela convient pour de petits exemples, mais ça ne tient plus dès qu’une fonction peut échouer de plusieurs façons distinctes : on ne peut pas faire de match sur une String, donc l’appelant ne peut pas savoir quel type d’échec s’est produit sans analyser le texte lui-même.
Le problème des erreurs String
Section intitulée « Le problème des erreurs String »fn withdraw(balance: f64, amount: f64) -> Result<f64, String> { if amount > balance { return Err(String::from("fonds insuffisants")); } if amount < 0.0 { return Err(String::from("le montant ne peut pas être négatif")); } Ok(balance - amount)}Si l’appelant veut réagir différemment à « fonds insuffisants » et à « montant négatif », sa seule possibilité est de comparer le texte de l’erreur. C’est fragile : tout casse dès que vous reformulez un message.
Définir une erreur avec une enum
Section intitulée « Définir une erreur avec une enum »Une enum règle ce problème : chaque type d’échec devient sa propre variante, sur laquelle l’appelant peut faire un match directement :
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!("Nouveau solde : {remaining}"), Err(WithdrawError::InsufficientFunds) => println!("Pas assez d'argent"), Err(WithdrawError::NegativeAmount) => println!("Impossible de retirer un montant négatif"), }}Sortie :
Pas assez d'argentDésormais, le compilateur s’assure que chaque type d’échec est traité : c’est la même garantie que match vous a toujours offerte avec les enums, appliquée cette fois aux erreurs.
Implémenter Display pour un vrai message d’erreur
Section intitulée « Implémenter Display pour un vrai message d’erreur »Essayez d’afficher WithdrawError avec {} tel quel : le code ne compile pas, car Rust ne sait pas comment le transformer en texte. Implémenter le trait Display règle ce problème. Les traits auront bientôt leurs propres pages (Les traits et Les traits courants). Pour l’instant, vous pouvez recopier ce modèle tel quel :
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, "fonds insuffisants"), WithdrawError::NegativeAmount => write!(f, "le montant ne peut pas être négatif"), } }}
fn main() { let error = WithdrawError::InsufficientFunds; println!("Erreur : {error}");}Sortie :
Erreur : fonds insuffisantsMaintenant, WithdrawError se comporte comme n’importe quelle autre erreur que vous voudriez afficher à un utilisateur ou écrire dans un journal, tout en restant entièrement utilisable dans un match pour le code qui doit réagir à un cas précis.
Quand une crate peut vous aider
Section intitulée « Quand une crate peut vous aider »Écrire à la main Display et tout le code répétitif autour d’un type d’erreur personnalisé devient vite lassant dans un vrai projet. La crate thiserror (en anglais) génère l’essentiel de ce code pour vous grâce à un #[derive], et anyhow (en anglais) est très utilisée dans le code d’application qui a seulement besoin de propager des erreurs sans définir un type pour chacune. Il est bon de savoir qu’elles existent, mais vous n’en avez pas besoin pour apprendre les bases. Quand vous serez prêt, la page anyhow et thiserror montre les deux en action, et quand choisir l’une ou l’autre.
Exercices
Section intitulée « Exercices »1. Les erreurs de connexion
Section intitulée « 1. Les erreurs de connexion »Définissez une enum LoginError avec deux variantes, EmptyUsername et PasswordTooShort, et implémentez Display pour elle. Écrivez ensuite fn check_login(user: &str, password: &str) -> Result<String, LoginError>, qui renvoie Ok avec un message de bienvenue, ou la bonne erreur. Un mot de passe doit faire au moins 8 caractères. Indice : .is_empty() et .len() sur un &str vous donnent ce qu’il faut pour vérifier.
Voir la solution
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, "le nom d'utilisateur ne peut pas être vide"), LoginError::PasswordTooShort => write!(f, "le mot de passe doit faire au moins 8 caractères"), } }}
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!("Bienvenue, {user} !"))}
fn main() { let attempts = [("ana", "correct-horse"), ("", "secret123"), ("ben", "court")];
for (user, password) in attempts { match check_login(user, password) { Ok(message) => println!("{message}"), Err(e) => println!("Erreur : {e}"), } }}Sortie :
Bienvenue, ana !Erreur : le nom d'utilisateur ne peut pas être videErreur : le mot de passe doit faire au moins 8 caractèresLes mots de passe d’essai n’ont pas d’accent : len() compte des octets, et une lettre accentuée en occupe deux.
2. Des erreurs qui transportent des détails
Section intitulée « 2. Des erreurs qui transportent des détails »Écrivez fn parse_percent(input: &str) -> Result<u32, PercentError>, où PercentError a deux variantes qui contiennent des données : NotANumber(String) (le texte invalide) et TooLarge(u32) (le nombre qui dépassait 100). Faites en sorte que les messages de Display incluent ces détails. Indice : les variantes d’une enum peuvent contenir des valeurs, comme sur la page Les enums. Dans fmt, écrivez PercentError::TooLarge(n) => ... dans le match pour récupérer la valeur.
Voir la solution
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}' n'est pas un nombre"), PercentError::TooLarge(n) => write!(f, "{n} dépasse 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", "moitié", "250"] { match parse_percent(input) { Ok(n) => println!("{n} %"), Err(e) => println!("Erreur : {e}"), } }}Sortie :
75 %Erreur : 'moitié' n'est pas un nombreErreur : 250 dépasse 100Récapitulatif
Section intitulée « Récapitulatif »- Une erreur
Stringfonctionne pour de petits exemples, mais l’appelant ne peut pas distinguer les cas avec unmatch. - Définissez une enum avec une variante par type d’échec, pour que l’appelant puisse faire un
matchsur l’erreur précise. - Implémentez
Display(impl fmt::Display for VotreErreur) pour pouvoir afficher le type d’erreur avec{}. - Dans les projets plus importants, des crates comme
thiserroretanyhowsuppriment l’essentiel du code répétitif.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Pourquoi une erreur de type String ne suffit-elle plus dès qu'une fonction peut échouer de plusieurs façons ?
2. Qu'est-ce qui permet à l'appelant de withdraw de réagir différemment à InsufficientFunds et à NegativeAmount ?
3. Que permet l'implémentation de Display pour un type d'erreur personnalisé ?
4. À quoi servent des crates comme thiserror et anyhow ?
5. Pourquoi comparer directement le texte des messages d'erreur est-il considéré comme fragile ?
Score : 0 / 5
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Defining an error type — Rust by Example (en anglais) : un court exemple exécutable de type d’erreur personnalisé avec
Display. - Wrapping errors — Rust by Example (en anglais) : l’étape suivante, une enum d’erreur qui enveloppe d’autres erreurs et fonctionne avec
?grâce àFrom. std::error::Error— documentation std (en anglais) : le trait standard qu’implémentent les « vrais » types d’erreur, et à quoi sert sa méthodesource().- To panic! or Not to panic! — The Rust Book (en anglais) : des recommandations pour savoir quand renvoyer une erreur et quand arrêter le programme.
- thiserror — docs.rs (en anglais) : la crate qui dérive
DisplayetErrorpour vos enums d’erreur, pour que vous écriviez beaucoup moins de code.