Gestion des erreurs en Rust : de unwrap aux techniques avancées
Contrairement aux langages avec exceptions (Java, Python, C++), Rust n'utilise pas le mécanisme try/catch. Au lieu de cela, le langage propose un système de types puissant et sûr pour la gestion des erreurs. Les développeurs débutants abusent souvent de unwrap(), ce qui provoque des paniques de l'application. Dans cet article, nous examinerons les techniques avancées de gestion des erreurs en Rust : de la création de types d'erreurs personnalisés à l'utilisation des crates thiserror et anyhow, et nous verrons comment émuler le comportement de try/catch.
1. Pourquoi Rust a-t-il abandonné try/catch ?
Dans les langages avec exceptions, le flux d'exécution peut être interrompu à tout moment. Cela complique l'analyse du code et peut entraîner des fuites de ressources. Rust propose deux approches principales :
- Erreurs récupérables — le type
Result<T, E>(succèsOk(T)ou erreurErr(E)). - Erreurs irrécupérables — la macro
panic!, qui arrête le programme.
L'opérateur ? est l'équivalent Rust de try. Il extrait la valeur de Ok ou retourne immédiatement Err depuis la fonction.
use std::fs::File;use std::io::Read;
fn read_username(path: &str) -> Result<String, std::io::Error> { let mut file = File::open(path)?; // si erreur — on la retourne let mut username = String::new(); file.read_to_string(&mut username)?; // encore "try" Ok(username.trim().to_string())}2. Création de types d'erreurs personnalisés
Pour les projets complexes, un seul std::io::Error ne suffit pas. La meilleure pratique est de définir son propre type d'erreur à l'aide d'une énumération (enum). Cela permet de regrouper différents types d'erreurs et d'ajouter du contexte.
use std::fmt;
#[derive(Debug)]enum MyAppError { IoError(std::io::Error), ParseError(String), NetworkError { url: String, status: u16 },}
impl fmt::Display for MyAppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { MyAppError::IoError(e) => write!(f, "Erreur IO : {}", e), MyAppError::ParseError(s) => write!(f, "Erreur d'analyse : {}", s), MyAppError::NetworkError { url, status } => { write!(f, "Erreur réseau pour {} : statut {}", url, status) } } }}
impl std::error::Error for MyAppError {} // on implémente le trait ErrorMaintenant, on peut utiliser ? pour une conversion automatique, si on implémente le trait From :
impl From<std::io::Error> for MyAppError { fn from(err: std::io::Error) -> MyAppError { MyAppError::IoError(err) }}
fn process_file(path: &str) -> Result<(), MyAppError> { let content = std::fs::read_to_string(path)?; // se convertit automatiquement if content.is_empty() { return Err(MyAppError::ParseError("fichier vide".into())); } Ok(())}3. Crates avancés : thiserror et anyhow
Écrire manuellement toutes les implémentations de Display et From est fastidieux. Pour cela, il existe deux crates populaires :
thiserror — pour les bibliothèques
La crate thiserror génère l'implémentation de std::error::Error via une macro derive. Idéale pour les bibliothèques où une description précise des erreurs est nécessaire.
use thiserror::Error;
#[derive(Error, Debug)]pub enum DataError { #[error("Erreur IO : {0}")] Io(#[from] std::io::Error), #[error("UTF-8 invalide : {0}")] Utf8(#[from] std::str::Utf8Error), #[error("Échec personnalisé : {details}")] Custom { details: String },}
// On peut maintenant écrire :fn parse_data(bytes: &[u8]) -> Result<String, Da