Retour au cours

backend / rust

Gestion d'erreurs : Result, Option, ?

Leçon 101 exercice

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.

ApprocheVerbositéCas d'usage
match expliciteVerbeux, mais montre tous les casQuand on veut réagir différemment selon l'erreur
Opérateur ?Une ligne, propage automatiquementQuand on veut juste transmettre l'échec à l'appelant
unwrap()Le plus court, mais panique si ErrPrototypes, 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.

rust
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) et Option<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érer unwrap_or, ?, ou un vrai match.
  • Les combinateurs (map, and_then, unwrap_or_else) permettent d'enchaîner les traitements sans imbrication.

Exercices pratiques

1 disponible
1

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 :

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

Résoudre l’exercice →