Phase 4Organisation du code

#16 Gestion des erreurs

Result, ?, erreurs custom

Panic vs Result

Rust offre deux mécanismes pour gérer les erreurs : panic! pour les erreurs irrécupérables et Result pour les erreurs récupérables.

panic! — erreur irrécupérable
fn main() {
    // panic! arrête immédiatement le programme
    // À utiliser seulement pour des bugs/situations impossibles
    panic!("Quelque chose de terrible est arrivé !");

    // Exemples qui causent une panique :
    // let v = vec![1, 2, 3];
    // v[99]; // index hors limites -> panic!
}
Result — erreur récupérable
use std::fs;

fn main() {
    // Result permet de gérer l'erreur proprement
    let contenu = fs::read_to_string("config.toml");

    match contenu {
        Ok(texte) => println!("Config : {}", texte),
        Err(e) => println!("Impossible de lire le fichier : {}", e),
    }

    // unwrap_or_else : fournir une valeur de repli
    let texte = fs::read_to_string("config.toml")
        .unwrap_or_else(|_| String::from("configuration par défaut"));

    println!("{}", texte);
}

Types d'erreur personnalisés

Pour les projets réels, on crée souvent ses propres types d'erreur qui regroupent toutes les erreurs possibles de l'application.

Enum d'erreur personnalisé
use std::fmt;
use std::io;
use std::num;

#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(num::ParseIntError),
    Custom(String),
}

// Implémenter Display pour afficher l'erreur
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "erreur I/O : {}", e),
            AppError::Parse(e) => write!(f, "erreur de parsing : {}", e),
            AppError::Custom(msg) => write!(f, "{}", msg),
        }
    }
}

// Implémenter std::error::Error
impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            AppError::Parse(e) => Some(e),
            AppError::Custom(_) => None,
        }
    }
}

L'opérateur ? en profondeur

L'opérateur ? fait plus que simplement propager les erreurs : il appelle automatiquement From::from() pour convertir le type d'erreur si nécessaire.

L'opérateur ? en chaîne
use std::fs;
use std::io;

fn lire_nombre_depuis_fichier(chemin: &str) -> Result<i32, AppError> {
    // ? convertit io::Error en AppError grâce à From
    let contenu = fs::read_to_string(chemin)?;

    // ? convertit ParseIntError en AppError grâce à From
    let nombre: i32 = contenu.trim().parse()?;

    Ok(nombre)
}

fn main() {
    match lire_nombre_depuis_fichier("nombre.txt") {
        Ok(n) => println!("Nombre lu : {}", n),
        Err(e) => println!("Erreur : {}", e),
    }
}

Conversion d'erreurs avec From

Le trait From permet à l'opérateur ? de convertir automatiquement les types d'erreur. Il suffit d'implémenter From pour chaque type d'erreur source.

Implémenter From
use std::io;
use std::num;

// Convertir io::Error en AppError
impl From<io::Error> for AppError {
    fn from(e: io::Error) -> AppError {
        AppError::Io(e)
    }
}

// Convertir ParseIntError en AppError
impl From<num::ParseIntError> for AppError {
    fn from(e: num::ParseIntError) -> AppError {
        AppError::Parse(e)
    }
}

// Maintenant ? fonctionne automatiquement !
fn charger_config(chemin: &str) -> Result<i32, AppError> {
    let contenu = fs::read_to_string(chemin)?;  // io::Error -> AppError
    let port: i32 = contenu.trim().parse()?;     // ParseIntError -> AppError
    Ok(port)
}

Le crate anyhow

Pour les applications (pas les bibliothèques), le crate anyhow simplifie énormément la gestion d'erreurs. Il fournit un type anyhow::Result qui accepte n'importe quelle erreur, et context() pour ajouter des messages explicatifs.

Cargo.toml
[dependencies]
anyhow = "1"
Utiliser anyhow
use anyhow::{Context, Result};
use std::fs;

fn lire_config(chemin: &str) -> Result<String> {
    let contenu = fs::read_to_string(chemin)
        .context(format!("erreur de lecture du fichier {}", chemin))?;

    Ok(contenu)
}

fn lire_port(chemin: &str) -> Result<u16> {
    let contenu = lire_config(chemin)?;
    let port: u16 = contenu.trim().parse()
        .context("le fichier ne contient pas un numéro de port valide")?;

    Ok(port)
}

fn main() {
    match lire_config("config.toml") {
        Ok(config) => println!("Config : {}", config),
        Err(e) => {
            println!("Erreur d'application : {}", e);
            // Afficher la chaîne de causes
            for cause in e.chain().skip(1) {
                println!("  Cause : {}", cause);
            }
        }
    }
}

À vous de jouer

Essayez les commandes ci-dessous pour compiler et exécuter le programme :

terminal — cargo
user@stemlegacy:~/error-handling-demo$