Aller au contenu

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.

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.

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'argent
▶ Essayez par vous-même

Dé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 insuffisants
▶ Essayez par vous-même

Maintenant, 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.

É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.

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 vide
Erreur : le mot de passe doit faire au moins 8 caractères
▶ Essayez par vous-même

Les mots de passe d’essai n’ont pas d’accent : len() compte des octets, et une lettre accentuée en occupe deux.

É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 nombre
Erreur : 250 dépasse 100
▶ Essayez par vous-même
  • Une erreur String fonctionne pour de petits exemples, mais l’appelant ne peut pas distinguer les cas avec un match.
  • Définissez une enum avec une variante par type d’échec, pour que l’appelant puisse faire un match sur 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 thiserror et anyhow suppriment l’essentiel du code répétitif.

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