Gestion des erreurs en Rust en 2026 : Result, Option, thiserror et anyhow

Guide complet sur la gestion des erreurs en Rust avec Result, Option, l'opérateur ?, thiserror pour les bibliothèques et anyhow pour les applications.

Gestion des erreurs Rust avec Result, Option, thiserror et anyhow

La gestion des erreurs en Rust repose sur deux énumérations fondamentales : Result<T, E> et Option<T>. Ces types obligent le développeur à traiter explicitement les cas de succès, d'échec et d'absence de valeur dès la compilation. Contrairement aux exceptions qui se propagent silencieusement, l'approche de Rust rend les chemins d'erreur visibles dans les signatures de fonctions, éliminant ainsi des catégories entières de surprises à l'exécution. L'opérateur ?, combiné aux crates thiserror et anyhow, simplifie ce modèle explicite sans sacrifier la clarté.

Quand utiliser lequel

Utiliser Option<T> pour les valeurs qui peuvent légitimement être absentes (champs de configuration, résultats de recherche). Utiliser Result<T, E> lorsque des opérations peuvent échouer avec des informations d'erreur significatives (entrées/sorties fichier, requêtes réseau, parsing).

Comprendre les fondamentaux de Result et Option

Result<T, E> représente soit un succès (Ok(T)), soit un échec (Err(E)). Option<T> représente soit une valeur (Some(T)), soit une absence (None). Les deux sont des types somme — le compilateur s'assure que chaque variante est traitée.

error_examples.rsrust
// Demonstrates Result and Option basic patterns

fn find_user(id: u64) -> Option<String> {
    // Returns None if user doesn't exist
    if id == 0 {
        None
    } else {
        Some(format!("User-{}", id))
    }
}

fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
    // Returns Err if parsing fails
    s.parse::<u16>()
}

fn main() {
    // Option handling - must address None case
    match find_user(42) {
        Some(name) => println!("Found: {}", name),
        None => println!("User not found"),
    }

    // Result handling - must address Err case
    match parse_port("8080") {
        Ok(port) => println!("Port: {}", port),
        Err(e) => println!("Invalid port: {}", e),
    }
}

Le compilateur rejette le code qui ignore ces valeurs de retour sans traitement explicite. Cette conception détecte les bugs à la compilation qui se manifesteraient autrement sous forme d'exceptions de pointeur nul ou d'erreurs non interceptées dans d'autres langages.

L'opérateur point d'interrogation pour une propagation concise

L'opérateur ? transforme les chaînes de match verbeuses en code linéaire lisible. Appliqué à un Result, il effectue un retour anticipé avec l'erreur si elle est présente, ou extrait la valeur de succès. Le même principe s'applique à Option.

file_reader.rsrust
// Using ? for clean error propagation

use std::fs::File;
use std::io::{self, BufRead, BufReader};

fn read_first_line(path: &str) -> Result<String, io::Error> {
    let file = File::open(path)?;  // Returns early if open fails
    let mut reader = BufReader::new(file);
    let mut line = String::new();
    reader.read_line(&mut line)?;  // Returns early if read fails
    Ok(line.trim().to_string())
}

fn get_port_from_config(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
    let content = read_first_line(path)?;
    let port = content.parse::<u16>()?;  // ParseIntError converts via From
    Ok(port)
}

L'opérateur ? exige que le type d'erreur soit convertible vers le type d'erreur de retour de la fonction via le trait From. L'utilisation de Box<dyn std::error::Error> comme montré ci-dessus accepte tout type d'erreur implémentant le trait Error standard.

Créer des erreurs personnalisées avec thiserror

La crate thiserror élimine le code répétitif pour les types d'erreur personnalisés. Elle dérive les implémentations de Error, Display et From via une macro procédurale.

errors.rsrust
// Custom error types using thiserror 2.0

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("configuration file not found at {path}")]
    NotFound { path: String },

    #[error("invalid port number: {0}")]
    InvalidPort(#[from] std::num::ParseIntError),

    #[error("IO error reading config")]
    IoError(#[from] std::io::Error),

    #[error("missing required field: {0}")]
    MissingField(String),
}

fn load_config(path: &str) -> Result<Config, ConfigError> {
    let content = std::fs::read_to_string(path)?;  // IoError auto-converts
    let port: u16 = content
        .lines()
        .find(|l| l.starts_with("port="))
        .ok_or(ConfigError::MissingField("port".into()))?
        .strip_prefix("port=")
        .unwrap()
        .parse()?;  // ParseIntError auto-converts to InvalidPort

    Ok(Config { port })
}

struct Config {
    port: u16,
}

L'attribut #[from] génère des conversions automatiques, permettant une utilisation fluide de ? avec différents types d'erreurs sous-jacents. Les messages d'erreur deviennent auto-documentés grâce aux chaînes de format #[error(...)].

Erreurs au niveau application avec anyhow

Alors que thiserror convient au code de bibliothèque avec des types d'erreur spécifiques, anyhow cible les applications où le contexte d'erreur importe plus que la granularité des types. Son trait Context ajoute des messages descriptifs à n'importe quelle erreur.

main.rsrust
// Application error handling with anyhow 1.0

use anyhow::{Context, Result, bail, ensure};

fn load_database_url() -> Result<String> {
    std::env::var("DATABASE_URL")
        .context("DATABASE_URL environment variable not set")
}

fn connect_to_database(url: &str) -> Result<DatabaseConnection> {
    ensure!(!url.is_empty(), "database URL cannot be empty");

    let conn = DatabaseConnection::new(url)
        .context("failed to establish database connection")?;

    if !conn.is_healthy() {
        bail!("database connection unhealthy after establishment");
    }

    Ok(conn)
}

fn main() -> Result<()> {
    let url = load_database_url()?;
    let conn = connect_to_database(&url)
        .context("application startup failed")?;

    // Context chains create readable error traces:
    // Error: application startup failed
    // Caused by:
    //     0: failed to establish database connection
    //     1: connection refused

    Ok(())
}

struct DatabaseConnection;
impl DatabaseConnection {
    fn new(_url: &str) -> Result<Self> { Ok(Self) }
    fn is_healthy(&self) -> bool { true }
}

La méthode context() encapsule les erreurs avec des informations supplémentaires, créant une chaîne qui facilite le débogage. La macro bail! fournit une sortie anticipée avec un message d'erreur formaté, tandis que ensure! agit comme une assertion qui retourne une erreur au lieu de paniquer.

Prêt à réussir tes entretiens Rust ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Combiner thiserror et anyhow dans des projets réels

Les bibliothèques exposent des erreurs structurées via thiserror pour un traitement programmatique par les consommateurs. Les applications encapsulent ces erreurs avec anyhow pour une sortie lisible par l'humain. Cette séparation maintient des APIs propres tout en préservant la débuggabilité.

lib.rs - Library code with thiserrorrust
// Exposes typed errors for programmatic handling

use thiserror::Error;

#[derive(Error, Debug)]
pub enum PaymentError {
    #[error("insufficient funds: required {required}, available {available}")]
    InsufficientFunds { required: u64, available: u64 },

    #[error("card declined: {reason}")]
    CardDeclined { reason: String },

    #[error("payment provider unavailable")]
    ProviderUnavailable(#[source] reqwest::Error),
}

pub fn process_payment(amount: u64) -> Result<Receipt, PaymentError> {
    // Library returns specific, matchable error types
    Err(PaymentError::InsufficientFunds {
        required: amount,
        available: 50,
    })
}

pub struct Receipt;
main.rs - Application code with anyhowrust
// Wraps library errors with context

use anyhow::{Context, Result};
use my_payment_lib::{process_payment, PaymentError};

fn checkout(cart_total: u64) -> Result<()> {
    match process_payment(cart_total) {
        Ok(_receipt) => Ok(()),
        Err(PaymentError::InsufficientFunds { required, available }) => {
            // Handle specific case differently
            println!("Add {} to your balance", required - available);
            Ok(())
        }
        Err(e) => Err(e).context("checkout payment processing failed"),
    }
}

Ce pattern permet aux appelants de faire correspondre des variantes spécifiques lorsque la récupération est possible, tout en bénéficiant d'un contexte d'erreur riche lors de la propagation des échecs vers le haut. La communauté Rust a largement standardisé cette approche, comme discuté dans les directives d'API Rust.

Patterns de gestion d'erreurs pour le code asynchrone

Les fonctions asynchrones retournent des Result exactement comme les fonctions synchrones. L'opérateur ? fonctionne de manière identique dans les blocs async, et thiserror comme anyhow s'intègrent sans modification.

async_errors.rsrust
// Error handling in async Rust with Tokio

use anyhow::{Context, Result};
use std::time::Duration;

async fn fetch_user_data(user_id: u64) -> Result<UserData> {
    let response = reqwest::get(format!("https://api.example.com/users/{}", user_id))
        .await
        .context("HTTP request to user API failed")?;

    let status = response.status();
    if !status.is_success() {
        anyhow::bail!("user API returned status {}", status);
    }

    let data: UserData = response
        .json()
        .await
        .context("failed to parse user data JSON")?;

    Ok(data)
}

async fn fetch_with_retry(user_id: u64, attempts: u32) -> Result<UserData> {
    let mut last_error = None;

    for attempt in 1..=attempts {
        match fetch_user_data(user_id).await {
            Ok(data) => return Ok(data),
            Err(e) => {
                last_error = Some(e);
                if attempt < attempts {
                    tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await;
                }
            }
        }
    }

    Err(last_error.unwrap()).context(format!("failed after {} attempts", attempts))
}

#[derive(serde::Deserialize)]
struct UserData {
    name: String,
}

Lors de la combinaison de plusieurs opérations asynchrones, utiliser try_join! de tokio ou futures pour les exécuter en parallèle tout en propageant la première erreur.

Downcasting et inspection des erreurs

anyhow::Error et Box<dyn Error> supportent tous deux le downcasting pour récupérer le type d'erreur original. Cela permet de journaliser des détails spécifiques tout en propageant des erreurs génériques.

downcasting.rsrust
// Inspecting wrapped error types

use anyhow::{Context, Result};
use std::io;

fn log_and_propagate(result: Result<()>) -> Result<()> {
    if let Err(ref e) = result {
        // Check if the root cause is a specific type
        if let Some(io_err) = e.downcast_ref::<io::Error>() {
            match io_err.kind() {
                io::ErrorKind::NotFound => {
                    tracing::warn!("file not found, using defaults");
                }
                io::ErrorKind::PermissionDenied => {
                    tracing::error!("permission denied - check file ownership");
                }
                _ => {
                    tracing::error!("IO error: {:?}", io_err);
                }
            }
        }
    }
    result
}

Le downcasting fait le pont entre la gestion d'erreurs générique et la logique de récupération spécifique. L'utiliser avec parcimonie — si le downcasting est fréquent, considérer si une énumération d'erreurs typées serait plus appropriée.

Conversion entre Option et Result

La bibliothèque standard fournit des méthodes pour convertir entre Option et Result, permettant une composition fluide lorsque différentes APIs utilisent des patterns différents.

conversions.rsrust
// Option and Result interoperability

fn get_env_port() -> Option<u16> {
    std::env::var("PORT")
        .ok()  // Result -> Option (discards error)
        .and_then(|s| s.parse().ok())
}

fn get_env_port_with_error() -> Result<u16, String> {
    std::env::var("PORT")
        .map_err(|_| "PORT not set".to_string())?
        .parse()
        .map_err(|_| "PORT is not a valid number".to_string())
}

fn lookup_and_parse(map: &std::collections::HashMap<String, String>, key: &str) -> Result<u16, String> {
    map.get(key)
        .ok_or_else(|| format!("key '{}' not found", key))?  // Option -> Result
        .parse()
        .map_err(|e| format!("parse error for '{}': {}", key, e))
}

La méthode ok() ignore les détails d'erreur lorsque seule la présence importe. Les méthodes ok_or() et ok_or_else() convertissent None en erreur personnalisée, permettant la propagation avec ? depuis les valeurs Option.

Considérations de performance

La gestion des erreurs en Rust ne coûte rien à l'exécution sur le chemin de succès. Result et Option sont des énumérations allouées sur la pile avec une taille déterministe. Le compilateur optimise les vérifications lorsqu'il peut prouver qu'une branche est inaccessible.

| Approche | Coût chemin succès | Coût chemin échec | |----------|-------------------|-------------------| | Result/Option | Zéro | Déroulement de pile (bon marché) | | panic! | Zéro | Déroulement complet de pile + nettoyage | | Exceptions C++ | Zéro (généralement) | Allocation heap coûteuse + RTTI |

Éviter unwrap() et expect() dans le code de bibliothèque — les réserver aux cas où l'échec indique véritablement un bug. Pour les chemins critiques en performance où les erreurs sont fréquentes, envisager d'utiliser des énumérations avec des données inline plutôt que des types d'erreur alloués sur le heap.

Conclusion

  • Result<T, E> gère les échecs récupérables ; Option<T> gère l'absence — les deux imposent un traitement à la compilation
  • L'opérateur ? propage les erreurs de manière concise, nécessitant des implémentations du trait From pour la conversion de types
  • thiserror génère des types d'erreur structurés pour les bibliothèques sans boilerplate
  • anyhow fournit des chaînes d'erreurs contextuelles pour les applications, supportant context(), bail! et ensure!
  • Combiner les deux : les bibliothèques exposent des erreurs typées via thiserror, les applications les encapsulent avec anyhow
  • Le code asynchrone utilise des patterns identiques — ? fonctionne dans les blocs async sans modification
  • Utiliser ok_or() pour convertir Option en Result ; utiliser ok() pour l'inverse
  • Faire du downcast sur les erreurs encapsulées uniquement lorsque la logique de récupération spécifique nécessite le type original

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#rust
#gestion-erreurs
#result
#option
#thiserror
#anyhow

Partager

Articles similaires