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.

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é.
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.
// 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.
// 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.
// 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.
// 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é.
// 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;// 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.
// 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.
// 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.
// 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 traitFrompour la conversion de types thiserrorgénère des types d'erreur structurés pour les bibliothèques sans boilerplateanyhowfournit des chaînes d'erreurs contextuelles pour les applications, supportantcontext(),bail!etensure!- Combiner les deux : les bibliothèques exposent des erreurs typées via
thiserror, les applications les encapsulent avecanyhow - Le code asynchrone utilise des patterns identiques —
?fonctionne dans les blocs async sans modification - Utiliser
ok_or()pour convertirOptionenResult; utiliserok()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
Partager
Articles similaires

Les smart pointers Rust expliqués : Box, Rc, Arc et RefCell en 2026
Les smart pointers Rust Box, Rc, Arc et RefCell expliqués avec des exemples compilables 2026, un tableau de décision et les questions d'entretien courantes.

Rust Edition 2024/2026 : Traits, Generics et Questions Avancees d'Entretien Technique
Guide complet sur les traits et generics Rust pour les entretiens techniques 2026 : trait upcasting, AsyncFn, RPITIT, pipelines de types et regles de capture des lifetimes.

Async/Await en Rust : Tokio, Futures et concurrence asynchrone en détail
Plongée approfondie dans async/await en Rust : le runtime Tokio, le trait Future, le lancement de tâches, la concurrence structurée et les patterns concrets pour construire des applications asynchrones performantes.