Gérer les erreurs récupérables avec Result

RustBeginner
Pratiquer maintenant

Introduction

Certaines défaillances sont dues à des erreurs de programmation, mais d’autres sont prévisibles lors de l’exécution : une personne peut saisir un texte mal formé ou une valeur en dehors de la plage autorisée. Rust représente ces résultats récupérables avec Result<T, E>, qui contient soit Ok(T), soit Err(E).

Vous allez analyser du texte valide et invalide sans provoquer de panique, renvoyer des erreurs de validation utiles et utiliser l’opérateur ? pour transmettre les échecs à l’appelant. Le projet Cargo préparé à l’avance permet de concentrer chaque modification sur la gestion des erreurs.

Faire correspondre un résultat réussi

Dans cette étape, vous allez convertir un texte en entier et faire correspondre le résultat obtenu.

Le projet se trouve dans /home/labex/project/result-workshop, et src/main.rs contient quatre emplacements réservés ciblés. Accédez au projet et ouvrez le fichier source :

cd /home/labex/project/result-workshop
nano src/main.rs

Remplacez le commentaire de l’étape 1 par :

    let valid = "42".parse::<i32>();
    match valid {
        Ok(value) => println!("Parsed value: {value}"),
        Err(error) => println!("Parse error: {error}"),
    }

parse est une méthode réutilisable qui peut produire différents types de sortie. La partie ::<i32> fournit le type de sortie de cet appel et demande à Rust d’interpréter la chaîne comme un entier signé de 32 bits. Cet argument de type entre chevrons est différent d’un chemin tel que Message::Text, même si les deux utilisent ::. L’analyse peut échouer : le type renvoyé est donc un Result. Ok(value) contient l’entier analysé ; Err(error) contient les informations expliquant l’échec. Un match doit traiter les deux variantes.

Enregistrez avec Ctrl+O, appuyez sur Entrée, puis quittez avec Ctrl+X. Vérifiez et exécutez le programme :

cargo check
cargo run --quiet
Parsed value: 42

Seule la branche Ok a été exécutée, car "42" est un texte représentant un entier valide.

Récupérer une entrée invalide

Dans cette étape, vous allez traiter un texte mal formé comme une donnée au lieu de le laisser provoquer l’arrêt du programme.

Une panique indique que le programme a atteint un état que sa conception ne prévoyait pas et qu’il arrête généralement son exécution. Une entrée externe mal formée est prévisible : elle doit donc rester un Err que votre programme peut examiner et signaler. Ouvrez le fichier source :

nano src/main.rs

Remplacez le commentaire de l’étape 2 par :

    let invalid = "many".parse::<i32>();
    match invalid {
        Ok(value) => println!("Unexpected value: {value}"),
        Err(error) => println!("Handled invalid input: {error}"),
    }

Ce match couvre toujours les deux résultats possibles, mais le texte préparé sélectionne la branche Err. Enregistrez et quittez, puis exécutez :

cargo run --quiet

La deuxième ligne doit être :

Handled invalid input: invalid digit found in string

Le programme a continué normalement et a transformé l’erreur de l’analyseur en message utile. Évitez unwrap() avec des données externes : face à ce même Err, unwrap() provoquerait une panique au lieu de vous permettre de récupérer l’erreur.

Analyser et valider avec l’opérateur point d’interrogation

Dans cette étape, vous allez créer une fonction capable de renvoyer soit un pourcentage, soit un message d’erreur utile.

Le type Result<u32, String> signifie qu’en cas de réussite, le résultat contient un entier non signé, tandis qu’en cas d’échec, il contient un texte d’erreur possédé. L’opérateur ? récupère la valeur contenue dans Ok ; s’il rencontre Err, il renvoie immédiatement cette erreur depuis la fonction courante.

Ouvrez le fichier source :

nano src/main.rs

Ajoutez cette fonction au-dessus de main :

fn parse_percentage(text: &str) -> Result<u32, String> {
    let value = text
        .parse::<u32>()
        .map_err(|_| format!("not a whole number: {text}"))?;

    if value <= 100 {
        Ok(value)
    } else {
        Err(format!("outside 0..=100: {value}"))
    }
}

Lisez la fonction comme une courte séquence. Tout d’abord, parse::<u32>() tente de convertir le texte en nombre entier. Ensuite, map_err(...) transforme uniquement un échec d’analyse en une String plus explicite contenant l’entrée d’origine.

Puis, ? récupère le nombre en cas de réussite ou renvoie immédiatement cette nouvelle erreur depuis parse_percentage. Si l’analyse réussit, le if valide le nombre et renvoie soit Ok(value), soit un Err indiquant que la valeur est hors limites.

Le paramètre _ de la closure signifie que la valeur d’erreur fournie par l’analyseur est volontairement ignorée, car le nouveau message la remplace. Ici, _ est un paramètre ignoré ; précédemment, _ => était une branche générique de match et let _ = supprimait un résultat complet.

Remplacez le commentaire de l’étape 3 dans main par :

    println!("85 => {:?}", parse_percentage("85"));
    println!("150 => {:?}", parse_percentage("150"));
    println!("many => {:?}", parse_percentage("many"));

Le formateur :? affiche la variante Ok ou Err dans le cadre de cette expérience pédagogique. Enregistrez et quittez, puis vérifiez et exécutez :

cargo check
cargo run --quiet

Les nouvelles lignes doivent être :

85 => Ok(85)
150 => Err("outside 0..=100: 150")
many => Err("not a whole number: many")

La fonction distingue maintenant une valeur valide, une valeur correctement analysée mais hors limites et un texte mal formé.

Propager une erreur à travers une autre fonction

Dans cette étape, vous allez utiliser ? à la frontière d’une deuxième fonction et traiter le résultat final dans main.

Une fonction auxiliaire ne sait pas toujours comment l’application doit afficher ou traiter une erreur. Elle peut propager le Err vers l’appelant et lui laisser le choix de la réponse. Ouvrez le fichier source :

nano src/main.rs

Ajoutez cette fonction entre parse_percentage et main :

fn acceptance_message(text: &str) -> Result<String, String> {
    let value = parse_percentage(text)?;
    Ok(format!("accepted {value}%"))
}

Si l’analyse ou la validation échoue, ? renvoie immédiatement la String d’erreur existante. En cas de réussite, la fonction place son message dans Ok.

Remplacez le commentaire de l’étape 4 dans main par :

    for text in ["73", "bad"] {
        match acceptance_message(text) {
            Ok(message) => println!("{text} => {message}"),
            Err(message) => println!("{text} => error: {message}"),
        }
    }

La boucle fournit une entrée valide et une entrée qui provoque un échec. main est la frontière qui transforme chaque résultat en sortie destinée à l’utilisateur. Enregistrez et quittez, puis vérifiez et exécutez :

cargo check
cargo run --quiet

Les dernières lignes doivent être :

73 => accepted 73%
bad => error: not a whole number: bad

Le même contexte d’erreur créé par parse_percentage a été conservé pendant sa propagation à travers acceptance_message.

Résumé

Vous avez fait correspondre des valeurs Result réussies et en échec, traité une entrée mal formée comme une situation récupérable, créé des erreurs String contextualisées avec map_err et propagé des échecs à travers les limites entre fonctions avec ?, au lieu de provoquer une panique avec unwrap.