📍 Guide principal de la série : Apprendre Rust de zéro : le guide complet pour débuter. Ce tutoriel fait partie de la série « Rust de zéro ». Pour la vue d’ensemble, lisez d’abord le guide principal.
Échouer proprement, sans exceptions cachées
Tout programme réel rencontre des situations qui peuvent mal tourner : un fichier d’inventaire introuvable, une quantité saisie qui n’est pas un nombre, un produit absent du stock. La plupart des langages traitent ces cas avec des exceptions — un mécanisme invisible qui « remonte » jusqu’à ce que quelqu’un l’attrape, ou fait planter le programme si personne ne le fait. Rust prend le parti inverse : une opération qui peut échouer le dit dans son type de retour. Impossible d’ignorer une erreur par mégarde, parce que le compilateur vous oblige à la regarder en face.
Cette approche déroute au début, puis devient un confort. Dans ce tutoriel, vous apprenez à distinguer les erreurs irrécupérables (qui justifient un arrêt) des erreurs récupérables (qu’on gère), à manier le type Result, et surtout à propager une erreur élégamment avec l’opérateur ?. Le fil conducteur : faire lire à stock-cli une quantité tapée par Ousmane et un fichier d’inventaire, deux opérations qui peuvent échouer.
🎯 Ce que vous allez apprendre
- Distinguer une erreur irrécupérable (
panic!) d’une erreur récupérable (Result). - Traiter un
Result<T, E>avecmatch, et savoir quandunwrap/expectsont acceptables. - Propager une erreur sans verbiage grâce à l’opérateur
?. - Lire un fichier et convertir une chaîne en nombre en gérant proprement l’échec.
🛠️ Ce que vous allez construire
Deux fonctions robustes pour stock-cli : lire_quantite, qui convertit une saisie texte en nombre et signale gentiment une saisie invalide, et charger_inventaire, qui lit un fichier et propage proprement l’erreur si le fichier manque. Le tout sans un seul plantage brutal non maîtrisé.
Prérequis
- Connaître
Option, lematchet les enums, carResulten est un. - Test express : si vous savez déballer un
Optionavecmatch, vous êtes prêt pourResult. - ⏱️ Temps estimé : ~40 minutes.
Étape 1 — Deux familles d’erreurs
Avant tout code, fixons le vocabulaire, car Rust sépare nettement deux situations. Une erreur irrécupérable est un bug ou une invariant violé dont on ne peut pas se remettre : continuer n’aurait aucun sens. On l’exprime avec la macro panic!, qui arrête le programme en affichant un message. Une erreur récupérable, au contraire, est une situation prévisible et normale — un fichier absent, une saisie erronée — qu’on veut gérer sans tuer le programme. On l’exprime avec le type Result.
La règle de jugement est simple : si l’erreur peut arriver en usage normal et qu’on a une réaction sensée (réessayer, prévenir l’utilisateur, prendre une valeur par défaut), c’est un Result. Si l’erreur signale un état impossible qui révèle un bug, panic! est légitime. Confondre les deux mène soit à des programmes fragiles qui plantent pour un rien, soit à des erreurs silencieusement ignorées.
fn main() {
// erreur irrécupérable, volontairement déclenchée
panic!("État impossible : stock négatif détecté");
}
Lancé, ce programme s’arrête en affichant le message et l’endroit du panic!. C’est utile pour signaler un bug pendant le développement, mais ce n’est pas ainsi qu’on traite une saisie invalide d’un utilisateur. Pour ça, place au Result.
Étape 2 — Le type Result, brique de la robustesse
Result<T, E> est un enum à deux variantes : Ok(valeur) en cas de succès (de type T) et Err(erreur) en cas d’échec (de type E). Beaucoup de fonctions de la bibliothèque standard renvoient un Result ; à vous de le traiter. Prenons la conversion d’une chaîne en nombre, omniprésente dès qu’on lit une saisie. La méthode parse renvoie justement un Result :
fn lire_quantite(saisie: &str) -> Result<u32, String> {
match saisie.trim().parse::<u32>() {
Ok(n) => Ok(n),
Err(_) => Err(format!("« {saisie} » n'est pas une quantité valide")),
}
}
fn main() {
match lire_quantite("12") {
Ok(n) => println!("Quantité reçue : {n}"),
Err(e) => println!("Erreur : {e}"),
}
match lire_quantite("douze") {
Ok(n) => println!("Quantité reçue : {n}"),
Err(e) => println!("Erreur : {e}"),
}
}
Ici, on transforme l’erreur technique de parse en un message clair pour Ousmane. La saisie "12" donne Ok(12), la saisie "douze" donne un Err explicatif. Le point fort : le code appelant ne peut pas utiliser le nombre sans avoir d’abord traité le cas d’échec — le compilateur l’impose. On ne « rate » jamais une erreur de conversion comme on le ferait avec un parseInt qui renvoie discrètement NaN.
✅ Point d’étape — Le programme doit afficher « Quantité reçue : 12 » puis « Erreur : … pas une quantité valide ». Si vous voyez une erreur de compilation
cannot infer type, c’est l’annotationparse::<u32>()qui manque :parsea besoin de savoir vers quel type convertir.
Étape 3 — unwrap et expect : pratiques mais à doser
Écrire un match complet à chaque Result est parfois lourd, surtout pour un prototype ou un cas où l’échec est vraiment impossible. Deux raccourcis existent : unwrap() renvoie la valeur si Ok, et panique si Err ; expect("message") fait pareil mais avec un message personnalisé, plus utile pour diagnostiquer.
let n: u32 = "12".parse().unwrap(); // OK : panique si échec
let m: u32 = "12".parse().expect("nombre attendu"); // message si échec
println!("{n} et {m}");
Ces deux méthodes sont pratiques pour apprendre, pour les tests, ou quand vous avez la certitude absolue que la valeur est valide (par exemple une constante du code). Mais en production, sur une donnée venue de l’extérieur, elles transforment une erreur récupérable en plantage : à proscrire. La règle saine : unwrap et expect dans les exemples et les tests, gestion explicite (ou propagation) dans le vrai code. Préférez toujours expect à unwrap : son message vous fera gagner du temps le jour où ça casse.
Étape 4 — L’opérateur ? : propager sans verbiage
Quand une fonction appelle plusieurs opérations faillibles d’affilée, écrire un match à chaque étape devient illisible. L’opérateur ? résout ce problème avec une élégance rare : placé après une expression qui renvoie un Result, il extrait la valeur si Ok, et retourne immédiatement l’erreur de la fonction si Err. C’est de la propagation automatique d’erreur. Voyons-le pour charger l’inventaire depuis un fichier :
use std::fs;
fn charger_inventaire(chemin: &str) -> Result<String, std::io::Error> {
let contenu = fs::read_to_string(chemin)?; // si erreur, on la retourne
Ok(contenu)
}
La fonction fs::read_to_string renvoie un Result<String, std::io::Error>. Le ? après l’appel signifie : « si la lecture réussit, donne-moi la String ; sinon, sors de charger_inventaire en renvoyant cette erreur d’entrée/sortie ». Sans ?, il aurait fallu un match de cinq lignes. Avec, le chemin heureux reste lisible et l’erreur est propagée proprement vers l’appelant, qui décidera quoi en faire.
Pour que ? fonctionne, la fonction doit elle-même renvoyer un type compatible — un Result (ou un Option, car ? marche aussi avec Option : il renvoie None en cas d’absence). On ne peut pas utiliser ? dans une fonction qui renvoie un type simple comme u32 : le compilateur le refusera, car il n’y aurait nulle part où propager l’erreur.
Étape 5 — Faire remonter l’erreur jusqu’à main
Où s’arrête la propagation ? Souvent jusqu’à main, qui peut lui-même renvoyer un Result. Cela permet d’utiliser ? dans main et de laisser Rust afficher l’erreur et terminer avec un code de sortie non nul si quelque chose échoue :
use std::fs;
use std::error::Error;
fn charger_inventaire(chemin: &str) -> Result<String, Box<dyn Error>> {
let contenu = fs::read_to_string(chemin)?;
Ok(contenu)
}
fn main() -> Result<(), Box<dyn Error>> {
let inventaire = charger_inventaire("inventaire.txt")?;
println!("Inventaire chargé : {} octets", inventaire.len());
Ok(())
}
Le type Box<dyn Error> est un « fourre-tout » qui accepte n’importe quelle erreur implémentant le trait standard Error : pratique quand une fonction peut échouer de plusieurs façons. Ici, si inventaire.txt n’existe pas, le ? dans main renvoie l’erreur, Rust l’affiche lisiblement et le programme se termine avec un statut d’échec — sans panic! brutal, sans message cryptique.
✅ Point d’étape — Lancez le programme sans le fichier : il doit afficher une erreur du type
No such file or directoryet se terminer proprement. Créez ensuite uninventaire.txt: il doit afficher le nombre d’octets. Vous tenez une chaîne d’erreurs propagée de bout en bout.
Une question revient vite : faut-il toujours se contenter de Box<dyn Error> ? Pour débuter, oui, c’est le choix le plus simple et il couvre l’immense majorité des besoins. Mais sachez qu’il existe une étape suivante : définir son propre type d’erreur sous forme d’enum, avec une variante par cas d’échec métier (par exemple ErreurStock::FichierIntrouvable, ErreurStock::QuantiteInvalide). Cela rend les erreurs typées et permet à l’appelant de réagir différemment selon le cas, plutôt que de traiter un message texte opaque. C’est un cap qu’on franchit naturellement quand stock-cli grandit ; pour l’instant, retenez simplement que Result et ? restent identiques — seul le type E change. Cette continuité est précieuse : le réflexe acquis aujourd’hui sur Box<dyn Error> se transposera sans effort sur un type d’erreur sur mesure demain.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
the ? operator can only be used in a function that returns Result |
? dans une fonction au retour incompatible |
Faire renvoyer un Result ou Option à la fonction |
called `Result::unwrap()` on an `Err` value |
unwrap sur une opération qui a échoué |
Gérer l’erreur avec match ou propager avec ? |
type annotations needed sur parse |
Type cible non précisé | Écrire parse::<u32>() ou annoter la variable |
cannot infer type sur Box<dyn Error> |
Import manquant | Ajouter use std::error::Error; |
| Programme qui panique pour une saisie utilisateur | Usage de unwrap en production |
Remplacer par une gestion explicite du Err |
🌍 Adaptation au contexte ouest-africain
La gestion d’erreurs de Rust prend tout son sens sur des outils de terrain. Un logiciel de caisse ou d’inventaire tourne dans des conditions imparfaites : coupure de courant en pleine écriture de fichier, clé USB retirée trop tôt, saisie hâtive d’un employé pressé un jour de marché. Avec les exceptions invisibles d’autres langages, ces incidents se traduisent par des plantages déroutants et des données corrompues. Le type Result force le développeur à prévoir chacun de ces cas dès l’écriture, ce qui donne des outils qui dégradent en douceur plutôt que de s’effondrer — exactement ce qu’on veut quand le logiciel sert vraiment, sans technicien à proximité.
Côté pédagogie, prenez l’habitude, dès vos premiers programmes, de renvoyer des Result dans les fonctions qui lisent un fichier, parsent une entrée ou appellent le réseau. C’est un réflexe qui coûte deux caractères (?) et qui vous évitera des heures de débogage le jour où votre programme tournera sur la machine d’un client, loin de votre écran.
✅ Récapitulatif
Vous savez désormais distinguer une erreur irrécupérable (panic!) d’une erreur récupérable (Result), traiter un Result<T, E> avec match, et doser unwrap/expect (parfait pour apprendre, dangereux en production). Surtout, vous maîtrisez l’opérateur ? qui propage les erreurs sans alourdir le code, jusqu’à un main qui renvoie lui-même un Result. Pour stock-cli, vous pouvez lire une saisie et un fichier en gérant proprement chaque échec. Il ne reste qu’à assembler toutes ces briques — données, collections, comportements génériques — dans un outil complet : le sujet du dernier tutoriel.
🧾 Aide-mémoire
| Élément | Rôle |
|---|---|
panic!("...") |
Erreur irrécupérable : arrêt du programme |
Result<T, E> : Ok/Err |
Succès ou échec récupérable |
.unwrap() |
Déballe ou panique (tests/prototype) |
.expect("msg") |
Comme unwrap, avec message |
expr? |
Propage l’erreur à l’appelant |
parse::<u32>() |
Convertit une chaîne, renvoie un Result |
Box<dyn Error> |
Type d’erreur générique « fourre-tout » |
💪 À vous de jouer
Écrivez une fonction verifier_stock(saisie: &str) -> Result<u32, String> qui convertit la saisie en nombre, puis renvoie une erreur si la quantité dépasse 1000 (seuil d’alerte d’un stock improbable), sinon le nombre.
Voir une solution
fn verifier_stock(saisie: &str) -> Result<u32, String> {
let n: u32 = saisie.trim().parse()
.map_err(|_| format!("« {saisie} » n'est pas un nombre"))?;
if n > 1000 {
return Err(format!("Quantité {n} suspecte (> 1000)"));
}
Ok(n)
}
fn main() {
println!("{:?}", verifier_stock("50")); // Ok(50)
println!("{:?}", verifier_stock("5000")); // Err(...)
println!("{:?}", verifier_stock("abc")); // Err(...)
}
On combine ? et map_err (qui transforme le type d’erreur) pour convertir, puis on ajoute une règle métier avec un return Err anticipé. La fonction renvoie trois résultats distincts selon l’entrée.
Tutoriels frères
- Structs, enums et pattern matching —
ResultetOptionsont des enums, à connaître d’abord. - Collections, traits et génériques : l’outil CLI complet — l’assemblage final de stock-cli.
Pour aller plus loin
- 🔝 Retour au guide principal : Apprendre Rust de zéro : le guide complet
- Chapitre « Error Handling » du livre officiel : doc.rust-lang.org/book
- Tutoriel suivant suggéré : Collections, traits et génériques
FAQ
Q : Quand utiliser panic! plutôt que Result ?
R : panic! pour un état impossible qui révèle un bug (un invariant violé). Result pour toute erreur attendue en usage normal (fichier absent, saisie invalide, réseau coupé). En cas de doute, préférez Result : il laisse le choix à l’appelant.
Q : L’opérateur ? marche-t-il avec Option ?
R : Oui. Dans une fonction qui renvoie un Option, ? extrait la valeur d’un Some et retourne None si la valeur est absente. Le mécanisme est le même que pour Result.
Q : unwrap est-il toujours mauvais ?
R : Non. Il est parfait dans les tests, les exemples, et quand vous avez la garantie formelle que la valeur est valide. Le problème, c’est l’unwrap sur une donnée externe en production : là, il faut gérer ou propager.
Q : Comment renvoyer plusieurs types d’erreurs différents d’une même fonction ?
R : Le plus simple pour débuter est Box<dyn Error>, qui accepte toute erreur standard. Plus tard, vous pourrez définir votre propre enum d’erreurs ou utiliser une crate dédiée, mais ce n’est pas nécessaire au début.