Krys4lide/docs/errors.md

5.2 KiB

Gestion des erreurs

Ce document décrit comment les erreurs sont gérées dans le projet.

Gestion native

Par principe, en Rust, on évite au maximum la gestion par exception, ne la réservant qu'aux situations où un crash du programme est la meilleure solution. En temps normal, on renvoie des Result<Valeur, Erreur> (pour les situations réussite/erreur) ou des Option<Valeur> (pour les situations valeur non-nulle/nulle).

Quand on fait face à une situation d'erreur, on cherchera à la gérer de manière explicite (voir Récupération des erreurs) ou à la remonter à un niveau supérieur, généralement à l'aide de l'opérateur ?.

On évitera, par contre, au maximum de générer des exceptions (appelées "panics" en Rust), que ce soit par l'usage de panic! ou par des appels à des fonctions qui paniquent en cas d'erreur (comme unwrap ou expect).

De nombreux exemples des idiomes natifs de gestion des erreurs en Rust sont disponibles dans la documentation Rust by example.

Librairies de gestion des erreurs

Deux librairies sont utilisées pour gérer les erreurs dans le projet :

  • anyhow : qui permet de renvoyer des erreurs faiblement typées, mais très facile à enrichir avec des messages d'explication. On l'utilise pour communiquer facilement des erreurs de haut niveau avec un utilisateur final.
    use anyhow::{anyhow, Result};
    
    fn get_cluster_info() -> Result<ClusterInfo> {
        let data = fs::read_to_string("cluster.json")
            .with_context(|| "failed to read cluster config")?;
        let info: ClusterInfo = serde_json::from_str(&data)
            .with_context(|| "failed to parse cluster config")?;
        Ok(info)
    }
    
  • thiserror : qui fournit des macros pour définir des erreurs fortement typées. On l'utilise pour définir des erreurs spécifiques à une partie du code, contextualisées avec des données structurées plutôt que de simples messages d'erreurs, afin de favoriser la "récupération" face aux erreurs.
    use thiserror::Error;
    
    #[derive(Error, Debug)]
    pub enum DataStoreError {
        #[error("data store disconnected")]
        Disconnect(#[from] io::Error),
        #[error("the data for key `{0}` is not available")]
        Redaction(String),
        #[error("invalid header (expected {expected:?}, found {found:?})")]
        InvalidHeader {
            expected: String,
            found: String,
        },
        #[error("unknown data store error")]
        Unknown,
    }
    

Récupération des erreurs

Dans la mesure du possible, on essaie de privilégier la "récupération" face à une erreur plutôt que le "crash". Les stratégies de récupération sont :

  • Réessayer l'opération, tel quel ou avec des paramètres différents
  • Contourner l'opération, en la remplaçant par une autre
  • À défaut, informer l'utilisateur de l'erreur et :
    • arrêter / annuler l'opération en cours
    • ignorer l'erreur et continuer l'exécution

Quand on ne peut pas récupérer une erreur, on la remonte à un niveau supérieur, si besoin en la convertissant dans un type d'erreur plus générique et approprié au niveau considéré.

Conversion des erreurs

Quand on remonte une erreur à un niveau supérieur, on peut être amené à la convertir dans un type d'erreur plus générique et approprié au niveau considéré. Pour faciliter cette conversion, on implémente le trait From. Avec thiserror, on peut utiliser l'attribut #[from] ou le paramètre source pour automatiser l'implémentation de telles conversions.

On peut ensuite, lors de la gestion d'une erreur, on pourra :

  • soit directement renvoyer l'erreur à l'aide de l'opérateur ?, qui se chargera de la conversion ;
  • soit convertir l'erreur explicitement, par exemple en utilisant la méthode map_err sur un Result, en particulier quand on veut enrichir l'erreur avec des informations supplémentaires.

Usages exceptionnels de unwrap et expect

Provoquant des "panics" en cas d'erreur, les fonctions unwrap et expect ne doivent être utilisées que dans des cas exceptionnels :

  • Dans les tests, pour signaler une erreur de test
  • Au plus haut niveau de l'application, pour signaler une erreur fatale qui ne peut pas être récupérée
  • Dans des situations où l'erreur ne peut pas se produire, par exemple après une vérification de préconditions

Dans l'idéal, on préférera l'usage de expect à unwrap, car il permet de donner un message d'erreur explicite.

Ressources