backend / rust
Gestion d'erreurs : Result, Option, ?
Explication
Ce que vous allez apprendre
- Comprendre
Result<T, E>comme représentation explicite d'un succès ou d'un échec - Créer un type d'erreur custom qui implémente
std::error::Error - Propager une erreur automatiquement vers l'appelant avec l'opérateur
? - Utiliser des combinateurs (
map,and_then,unwrap_or) pour chaîner des traitements fallibles - Savoir quand
unwrap()est acceptable et quand il représente un risque en production
Dans quel contexte ?
Un développeur écrit une chaîne de traitement où chaque étape peut échouer : valider un âge, puis charger un profil, puis l'enregistrer en base. Sans l'opérateur ?, chaque étape nécessiterait un match explicite pour vérifier le succès avant de continuer — un code qui s'empile rapidement en pyramide illisible. Avec ?, chaque étape fallible tient sur une seule ligne, et l'erreur remonte automatiquement dès qu'elle survient.
D'abord, Rust n'a pas d'exceptions pour le flux normal, comme Go
Result<T, E> représente exactement deux possibilités : Ok(valeur) en cas de succès, Err(erreur) en cas d'échec. Le compilateur force à traiter les deux cas — impossible d'accéder directement à la valeur sans d'abord vérifier si elle est bien présente, contrairement à une exception qu'on pourrait négliger d'attraper.
Une fois ce principe posé, il faut connaître le raccourci le plus utilisé du langage : ?
let age_valide = valider_age(age)?; est équivalent à un match complet qui retournerait immédiatement l'erreur si valider_age échoue, ou continuerait avec la valeur si elle réussit. Cet opérateur ne fonctionne que dans une fonction dont le type de retour est lui-même un Result (ou Option) compatible.
| Approche | Verbosité | Cas d'usage |
|---|---|---|
match explicite | Verbeux, mais montre tous les cas | Quand on veut réagir différemment selon l'erreur |
Opérateur ? | Une ligne, propage automatiquement | Quand on veut juste transmettre l'échec à l'appelant |
unwrap() | Le plus court, mais panique si Err | Prototypes, cas où l'échec est réellement impossible |
Prérequis
Il faut être à l'aise avec les enums et le pattern matching (leçons précédentes) : Result et Option sont eux-mêmes des enums de la bibliothèque standard.
Il reste une erreur de jugement fréquente chez les débutants : abuser de unwrap()
unwrap() extrait la valeur d'un Result/Option, mais fait planter tout le programme (panic) si la valeur est en réalité un Err/None. C'est pratique pour prototyper vite, mais dangereux dans du code de production où une entrée imprévue provoquerait un crash évitable.
Piège dangereux
Un unwrap() sur un Result provenant d'une entrée externe (fichier, réseau, entrée utilisateur) est une bombe à retardement : le jour où cette entrée est invalide, le programme entier plante au lieu de gérer l'erreur proprement. Réserve unwrap() aux cas où l'échec est réellement structurellement impossible, ou aux tests.
Ensuite, les combinateurs évitent d'imbriquer des match
.map(|v| v * 2) transforme la valeur à l'intérieur d'un Ok sans toucher à un éventuel Err. .and_then(...) enchaîne une seconde opération fallible. .unwrap_or(valeur_defaut) extrait la valeur ou retourne un défaut sans jamais paniquer. Ces méthodes permettent d'enchaîner des traitements fallibles sans jamais imbriquer de match.
Bonne pratique
Implémente std::error::Error (via impl fmt::Display et une déclaration impl std::error::Error for MonErreur {}) pour tout type d'erreur custom destiné à une vraie bibliothèque : cela le rend compatible avec l'écosystème plus large, notamment Box<dyn std::error::Error> comme type de retour générique pour main().
Maintenant que tu sais gérer l'échec de façon rigoureuse, la prochaine leçon aborde un concept qui structure énormément de code Rust générique et réutilisable : les traits et les generics.
Commandes & code
Gestion d'erreurs : Result, Option, ?
Rust n'a pas d'exceptions : Option<T> gère l'absence, Result<T, E> gère l'échec.
use std::fmt;
// Result<T, E> : Ok(valeur) ou Err(erreur), tous deux doivent etre traites
fn diviser(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("division par zero"))
} else {
Ok(a / b)
}
}
// Erreur custom implementant std::error::Error (idiomatique pour une vraie librairie)
#[derive(Debug)]
enum ErreurValidation {
ChampVide(String),
ValeurInvalide { champ: String, raison: String },
}
impl fmt::Display for ErreurValidation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ErreurValidation::ChampVide(champ) => write!(f, "le champ '{}' est vide", champ),
ErreurValidation::ValeurInvalide { champ, raison } => {
write!(f, "champ '{}' invalide: {}", champ, raison)
}
}
}
}
impl std::error::Error for ErreurValidation {}
fn valider_age(age: i32) -> Result<u32, ErreurValidation> {
if age < 0 {
return Err(ErreurValidation::ValeurInvalide {
champ: "age".to_string(),
raison: "doit etre positif".to_string(),
});
}
Ok(age as u32)
}
// L'operateur ? propage l'erreur automatiquement si Result est Err
fn traiter_utilisateur(nom: &str, age: i32) -> Result<String, ErreurValidation> {
if nom.is_empty() {
return Err(ErreurValidation::ChampVide("nom".to_string()));
}
let age_valide = valider_age(age)?; // equivaut a: match ... { Err(e) => return Err(e), Ok(v) => v }
Ok(format!("{} a {} ans", nom, age_valide))
}
fn main() {
match diviser(10.0, 2.0) {
Ok(r) => println!("resultat: {}", r),
Err(e) => println!("erreur: {}", e),
}
// unwrap() : panique si Err/None, a reserver aux prototypes ou cas garantis surs
let r = diviser(10.0, 5.0).unwrap();
println!("{}", r);
// unwrap_or / unwrap_or_else : valeur par defaut sans paniquer
let r2 = diviser(10.0, 0.0).unwrap_or(0.0);
println!("{}", r2);
// Combinateurs : chainer les traitements sans if/else imbriques
let resultat = diviser(20.0, 4.0)
.map(|v| v * 2.0) // transforme Ok, laisse Err inchange
.and_then(|v| diviser(v, 2.0)) // chaine une autre operation fallible
.unwrap_or(-1.0);
println!("{}", resultat);
match traiter_utilisateur("Alice", 30) {
Ok(msg) => println!("{}", msg),
Err(e) => println!("erreur: {}", e),
}
match traiter_utilisateur("", 30) {
Ok(msg) => println!("{}", msg),
Err(e) => println!("erreur: {}", e),
}
// Option : meme logique pour l'absence de valeur
let peut_etre: Option<i32> = Some(5);
println!("{}", peut_etre.map(|v| v * 2).unwrap_or(0));
// main() peut lui-meme retourner un Result pour propager avec ? au top-level
// fn main() -> Result<(), Box<dyn std::error::Error>> { ... Ok(()) }
}Résumé
Result<T, E>(échec possible) etOption<T>(absence possible) forcent le traitement explicite des cas d'erreur.- L'opérateur
?propage automatiquement une erreur vers l'appelant — remplace les blocs try/catch. unwrap()panique et doit rester exceptionnel ; préférerunwrap_or,?, ou un vraimatch.- Les combinateurs (
map,and_then,unwrap_or_else) permettent d'enchaîner les traitements sans imbrication.
Exercices pratiques
Mission : un import CSV qui plante au premier fichier client
Objectif : Diagnostiquer un abus de unwrap() sur des donnees externes et le remplacer par une propagation d'erreur idiomatique avec ?.
Contexte
Un outil d'import de fichiers CSV clients fonctionnait parfaitement en demo, avec des fichiers prepares a l'avance. Des le premier vrai fichier client, envoye par un partenaire externe avec une ligne d'age mal formatee ('N/A' au lieu d'un nombre), le programme entier crashe brutalement en production, sans aucun message exploitable dans les logs :
fn lire_age(valeur: &str) -> u32 {
valeur.parse::<u32>().unwrap()
}Ton equipe te demande de rendre l'import resilient : une ligne invalide doit etre signalee proprement, sans faire planter tout l'import des milliers de lignes suivantes.