From 1561fd2a449cb14f5305cdc08be59ed843804245 Mon Sep 17 00:00:00 2001 From: Florian Briand Date: Tue, 20 Aug 2024 22:33:57 +0200 Subject: [PATCH] feat: add documentation about errors handling in docs/errors.md --- docs/errors.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/errors.md diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..8f1f342 --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,85 @@ +# 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` (pour les situations réussite/erreur) ou des `Option` (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](#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](https://doc.rust-lang.org/rust-by-example/error.html). + +## Librairies de gestion des erreurs + +Deux librairies sont utilisées pour gérer les erreurs dans le projet : +- [`anyhow`](https://docs.rs/anyhow/latest/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. + ```rust + use anyhow::{anyhow, Result}; + + fn get_cluster_info() -> Result { + 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`](https://docs.rs/thiserror/latest/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. + ```rust + 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 + +- [The Rust Programming Language - Ch. 9: Error Handling](https://doc.rust-lang.org/book/ch09-00-error-handling.html) +- [The NRC Book - Error Handling in Rust](https://nrc.github.io/error-docs/intro.html) +- [The Error Design Patterns Book - by Project Error Handling WG](https://github.com/rust-lang/project-error-handling/blob/master/error-design-patterns-book/src/SUMMARY.md) +- [The Rust Cookbook - Error Handling](https://rust-lang-nursery.github.io/rust-cookbook/error-handling.html) +- [Le ticket initial de l'intégration de la gestion des erreurs dans le projet](https://forge.p4pillon.org/P4Pillon/Krys4lide/issues/34)