Vol. 4 · № 01 · Un atelier RustA Rust workshop

Result & l'opérateur ?, ou l'erreur comme valeur Result & the ? operator, or the error as a value

L'erreur est dans le type de retour, pas dans un canal parallèle invisible comme l'exception. La distinction qui structure tout : erreur attendue vs bug. The error lives in the return type, not in an invisible parallel channel like the exception. The distinction that structures everything: expected error vs bug.

AudienceAudience
Dev à l'aise avec enums et Option (Vol 3) Dev comfortable with enums and Option (Vol 3)
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●○○ ErreursResult?
SommaireContents ·
№ 01CadrageFraming3 min

L'échec n'est pas une interruption surprise. C'est une valeur de retour.Failure isn't a surprise interruption. It's a return value.

Le Vol 3 a appris à rendre l'illégal impossible à écrire. Mais toutes les erreurs ne sont pas des bugs à proscrire : un fichier qui n'existe pas, un réseau qui tombe, une saisie invalide sont des événements normaux. La plupart des langages les signalent par une exception — un canal parallèle, invisible dans la signature, qui saute par-dessus ton code jusqu'à ce que quelqu'un l'attrape, ou pas. Rust prend le chemin inverse : l'échec est une valeur ordinaire, Result<T, E>, posée dans le type de retour. Le même enum que partout — Ok ou Err — au cœur de la gestion d'erreur la plus honnête qui soit.Vol 3 taught us to make the illegal impossible to write. But not every error is a bug to forbid: a file that isn't there, a network that drops, invalid input are normal events. Most languages signal them with an exception — a parallel channel, invisible in the signature, that leaps over your code until someone catches it, or doesn't. Rust takes the opposite path: failure is an ordinary value, Result<T, E>, placed in the return type. The same enum as everywhere — Ok or Err — at the heart of the most honest error handling there is.

Result — l'enum d'Option, mais l'échec porte sa raisonResult — Option's enum, but failure carries its reason
enum Result<T, E> {   // exactement la forme d'Option, mais l'absence porte une RAISON
    Ok(T),            // le succès, qui transporte la valeur attendue
    Err(E),           // l'échec, qui transporte ce qui a mal tourné
}

// lire un fichier peut échouer : le type de retour le DIT
fn lire_config(chemin: &str) -> Result<String, std::io::Error> { /* ... */ }

let config = lire_config("app.toml");   // config est Result<String, Error>
// impossible d'utiliser le String sans d'abord regarder le cas Err en face.
Attendue, ou bug ?Expected, or bug?

La question qui structure tout le volume : est-ce une erreur attendue, ou un bug ? Un fichier absent, un port occupé, un JSON malformé : ce sont des conditions du monde réel, prévisibles — elles appartiennent au type de retour, en Result. Un invariant qu'on s'était promis et qui se rompt (un index hors borne, un état qui ne devrait jamais arriver) : ça, c'est un bug, et ça relève du panic — le sujet du n°3. Confondre les deux est la première erreur de conception : on ne met pas un bug dans un Result, et on ne fait pas paniquer sur un fichier manquant.The question that structures the whole volume: is this an expected error, or a bug? A missing file, a busy port, malformed JSON: these are predictable real-world conditions — they belong in the return type, as Result. An invariant you promised yourself that breaks (an out-of-bounds index, a state that should never happen): that's a bug, and it's the realm of panic — issue n°3's subject. Confusing the two is the first design mistake: you don't put a bug in a Result, and you don't panic on a missing file.

Le réflexe du numéroThe issue's reflex

« Cette fonction peut-elle échouer pour une raison normale ? » Si oui, elle renvoie un Result, et le compilateur force l'appelant à traiter l'échec — exactement comme Option forçait à traiter l'absence. L'erreur cesse d'être une chose qui arrive à ton code pour devenir une chose qui est dans ton code, qu'on lit, transforme et propage.'Can this function fail for a normal reason?' If so, it returns a Result, and the compiler forces the caller to handle the failure — exactly as Option forced handling absence. The error stops being something that happens to your code and becomes something that is in your code, read, transformed and propagated.

Result, c'est Option avec un pourquoi.Result is Option with a why.

Option<T> dit « il y a une valeur, ou rien ». Result<T, E> dit « il y a une valeur, ou un échec — et voici lequel ». Là où None est muet, Err(e) transporte le diagnostic. Tout ce que tu as appris sur Option — match, if let, ?, les combinateurs — se transpose presque tel quel sur Result. C'est le même enum, enrichi d'une raison.Option<T> says 'there's a value, or nothing'. Result<T, E> says 'there's a value, or a failure — and here's which'. Where None is mute, Err(e) carries the diagnosis. Everything you learned about Option — match, if let, ?, the combinators — transfers almost unchanged to Result. It's the same enum, enriched with a reason.

№ 02La surpriseThe surprise4 min

L'exception saute par-dessus ton code. Result le traverse à découvert.The exception leaps over your code. Result goes through it in the open.

Une exception voyage dans un canal parallèle : elle ne figure pas dans le type de retour, elle remonte la pile d'appels en sautant par-dessus chaque fonction jusqu'à un catch — ou jusqu'au crash si personne ne l'attrape. Le lecteur d'une signature ne sait pas ce qui peut échouer ; il faut connaître l'implémentation, et celle de tout ce qu'elle appelle. Result fait l'inverse : l'échec est un cas du type de retour, visible, et le compilateur refuse de te laisser l'oublier. La différence n'est pas stylistique — c'est la différence entre une erreur qu'on peut ignorer par accident et une erreur qu'on doit décider de traiter.An exception travels in a parallel channel: it doesn't appear in the return type, it climbs the call stack leaping over each function until a catch — or until the crash if no one catches it. The reader of a signature doesn't know what can fail; you have to know the implementation, and that of everything it calls. Result does the opposite: failure is a case of the return type, visible, and the compiler refuses to let you forget it. The difference isn't stylistic — it's the difference between an error you can ignore by accident and an error you must decide to handle.

Le contre-exemple — une signature qui ment sur ce qui peut échouerThe counter-example — a signature that lies about what can fail
// Style « exception » (pseudo-code à la Java/Python)
fn charger_utilisateur(id: u32) -> Utilisateur {
    let ligne = db.query(id);        // peut lever DbError — RIEN ne le dit
    let u = parse(ligne);            // peut lever ParseError — RIEN ne le dit
    u
}

charger_utilisateur(42);             // ✓ COMPILE. l'échec est invisible.
// la signature promet un Utilisateur. elle ment : elle peut aussi
// faire exploser la pile d'appels par un canal que le type ne montre pas.
↟saute les cadresleaps the frames
main()attrape — ou crashcatches — or crashes
charger_utilisateur()traversée — ne voit rienpassed through — sees nothing
query() ✦lève l’exceptionthrows

La signature annonce -> Utilisateur, mais deux appels peuvent faire dérailler l'exécution sans que rien, au point d'appel, ne l'indique. Pour savoir ce qui peut échouer, il faut ouvrir le corps de la fonction — et celui de query, et de parse, récursivement. L'échec existe, mais il s'est rendu invisible. C'est précisément l'exception non gérée que tant de programmes traînent : non par malveillance, mais parce que le type ne forçait personne à y penser.The signature announces -> Utilisateur, but two calls can derail execution without anything at the call site indicating it. To know what can fail, you must open the body of the function — and that of query, and of parse, recursively. The failure exists, but it made itself invisible. This is exactly the unhandled exception so many programs carry: not out of malice, but because the type forced no one to think about it.

Une exception, c'est un échec qu'on peut ignorer par accident. Un Result, c'est un échec qu'on doit décider d'ignorer.An exception is a failure you can ignore by accident. A Result is a failure you must decide to ignore.
№ 03La mécaniqueThe mechanics5 min

L'opérateur ? : la propagation d'une exception, mais écrite.The ? operator: an exception's propagation, but spelled out.

Sans aide, propager une erreur à la main est verbeux : chaque appel faillible devient un match qui, en cas d'Err, retourne. Le code se noie sous la plomberie, et le chemin heureux disparaît. L'opérateur ? est la réponse de Rust : placé après une expression qui rend un Result, il extrait le Ok et continue, ou retourne l'Err immédiatement à l'appelant. On retrouve la légèreté de l'exception — une ligne droite qui se lit comme si rien ne pouvait échouer — sans en perdre l'honnêteté : chaque ? est un point de sortie visible, et la signature porte toujours le type d'erreur.Without help, propagating an error by hand is verbose: each fallible call becomes a match that, on Err, returns. The code drowns in plumbing, and the happy path vanishes. The ? operator is Rust's answer: placed after an expression yielding a Result, it extracts the Ok and continues, or returns the Err immediately to the caller. You regain the exception's lightness — a straight line that reads as if nothing could fail — without losing its honesty: each ? is a visible exit point, and the signature still carries the error type.

Ce que ? veut dire — le match que tu n'écris plusWhat ? means — the match you no longer write
// Ce qu'on écrit
let octets = std::fs::read(chemin)?;

// Ce que ? veut dire, exactement
let octets = match std::fs::read(chemin) {
    Ok(v)  => v,                 // succès : on extrait la valeur, on continue
    Err(e) => return Err(e.into()),  // échec : on retourne TOUT DE SUITE
};
// ? = « donne-moi le Ok, ou propage l'Err à l'appelant ». Un caractère.
? convertit l'erreur au passage (via From)? converts the error on the way (via From)
fn charger(chemin: &str) -> Result<Config, AppError> {
    let texte = std::fs::read_to_string(chemin)?;   // io::Error -> AppError
    let config: Config = toml::from_str(&texte)?;   // toml::Error -> AppError
    Ok(config)
}
// deux types d'erreur différents, une seule signature : le ? appelle
// From pour convertir chaque erreur en AppError au passage. La chaîne
// se lit comme le chemin heureux ; les échecs sortent par le côté.

Le détail qui rend ? si puissant : il appelle From sur l'erreur avant de la retourner. Une fonction qui peut rencontrer une io::Error et une toml::Error n'expose qu'un seul type, AppError ; chaque ? convertit l'erreur rencontrée vers ce type commun, à condition qu'un From existe. Concevoir ce type d'erreur unifié — et ses conversions — est précisément le sujet du n°2.The detail that makes ? so powerful: it calls From on the error before returning it. A function that may meet an io::Error and a toml::Error exposes a single type, AppError; each ? converts the error it meets into that common type, provided a From exists. Designing that unified error type — and its conversions — is exactly issue n°2's subject.

Quand ne pas propagerWhen not to propagate

Le ? remonte l'erreur ; ce n'est pas toujours ce qu'on veut. Parfois l'échec a une réponse locale : une valeur par défaut (unwrap_or), une autre source à essayer (or_else), un log puis on continue. Comme pour Option, les combinateurs — map, map_err, and_then, ok() — transforment le Result sans le déballer. ? est pour « je ne sais pas traiter ça ici, que l'appelant décide » ; les combinateurs, pour « je sais quoi en faire, maintenant ».The ? bubbles the error up; that's not always what you want. Sometimes failure has a local answer: a default value (unwrap_or), another source to try (or_else), a log then carry on. As with Option, the combinators — map, map_err, and_then, ok() — transform the Result without unwrapping it. ? is for 'I can't handle this here, let the caller decide'; combinators, for 'I know what to do with it, now'.

? marche aussi sur Option.? works on Option too.

Le même opérateur fonctionne sur Option : dans une fonction qui rend Option<T>, un ? sur un None retourne None tout de suite. C'est la preuve que Result et Option sont deux variations d'une même idée — l'enum qui force à regarder l'autre cas. ? est juste « extrais le cas heureux, ou sors par le côté », quel que soit l'enum.The same operator works on Option: in a function returning Option<T>, a ? on a None returns None at once. It's proof that Result and Option are two variations on one idea — the enum that forces you to face the other case. ? is just 'extract the happy case, or exit through the side', whichever enum it is.

Le ? rend le chemin heureux lisible et les échecs explicites — la légèreté de l'exception, sans son invisibilité.The ? keeps the happy path readable and failures explicit — the exception's lightness, without its invisibility.
№ 04Le piègeThe trap4 min

Le .unwrap() partout : l'exception non gérée, réinventée.The .unwrap() everywhere: the unhandled exception, reinvented.

Le compilateur refuse d'extraire la valeur d'un Result sans traiter l'Err. Face à ce mur, la tentation est immédiate : .unwrap(). Une méthode qui dit « donne-moi le Ok, et si c'est Err, fais paniquer le programme ». Le code compile, l'erreur disparaît de la vue — et l'on vient de rétablir exactement ce que Result voulait nous épargner : un échec attendu qui termine le programme par un crash, sans contexte, au pire moment. Le compilateur t'avait tendu une question — « que fais-tu si ça échoue ? » — et .unwrap() y répond « je plante ». Sur un bug, c'est parfois correct. Sur un fichier manquant, c'est une faute.The compiler refuses to extract a Result's value without handling the Err. Faced with this wall, the temptation is immediate: .unwrap(). A method that says 'give me the Ok, and if it's Err, panic the program'. The code compiles, the error vanishes from sight — and you've just restored exactly what Result meant to spare you: an expected failure that ends the program in a crash, with no context, at the worst moment. The compiler had handed you a question — 'what do you do if this fails?' — and .unwrap() answers 'I crash'. On a bug, that's sometimes right. On a missing file, it's a mistake.

Le contre-exemple — trois unwrap, trois crashs possibles en prodThe counter-example — three unwraps, three possible production crashes
// .unwrap() partout « pour faire passer le compilateur »
fn charger_port() -> u16 {
    let texte = std::fs::read_to_string("config").unwrap();  // panique si absent
    let config: Config = toml::from_str(&texte).unwrap();    // panique si invalide
    config.port.unwrap()                                     // panique si manquant
}
// le compilateur était satisfait. l'utilisateur, lui, reçoit un crash brut
// sur un fichier manquant — exactement l'exception non gérée qu'on fuyait.

Chaque .unwrap() transforme un échec attendu en panique. Le fichier de config peut manquer, être mal formé, ne pas contenir de port : ce sont des situations normales d'exploitation, pas des bugs. Les traiter par .unwrap(), c'est dire au programme « si le monde réel se présente, meurs ». On a fait taire le compilateur sans répondre à sa question — et on a rétabli l'exception non gérée que tout le volume cherche à éliminer.Each .unwrap() turns an expected failure into a panic. The config file may be missing, malformed, hold no port: these are normal operating situations, not bugs. Handling them with .unwrap() tells the program 'if the real world shows up, die'. You silenced the compiler without answering its question — and restored the unhandled exception the whole volume sets out to eliminate.

Le bon réflexe — propager l'échec attendu, laisser l'appelant déciderThe right reflex — propagate the expected failure, let the caller decide
// On traite l'échec attendu comme une valeur, et on le propage
fn charger_port() -> Result<u16, AppError> {
    let texte = std::fs::read_to_string("config")?;   // l'absence remonte, typée
    let config: Config = toml::from_str(&texte)?;     // l'invalidité remonte aussi
    config.port.ok_or(AppError::PortManquant)         // Option -> Result, avec raison
}
// l'appelant choisit : valeur par défaut, message clair, nouvelle tentative.
// rien ne panique sur une condition que le monde réel produit normalement.

Le ? remonte chaque échec à l'appelant, et ok_or transforme l'Option du port en Result en lui donnant une raison. La fonction redevient honnête : sa signature dit qu'elle peut échouer, et le code de plus haut niveau — celui qui sait quoi faire d'un défaut de config — décide. La règle n'est pas « jamais d'unwrap » mais « pas d'unwrap sur une erreur attendue ». Le n°3 dira où unwrap et panic sont, eux, le bon outil : sur un invariant brisé, c'est-à-dire un vrai bug.The ? bubbles each failure to the caller, and ok_or turns the port's Option into a Result by giving it a reason. The function is honest again: its signature says it can fail, and higher-level code — the one that knows what to do with a config fault — decides. The rule isn't 'never unwrap' but 'no unwrap on an expected error'. Issue n°3 will say where unwrap and panic are the right tool: on a broken invariant — that is, a real bug.

L'alternative honnête à unwrapThe honest alternative to unwrap

Avant d'écrire .unwrap(), passe en revue : ? (propage), unwrap_or / unwrap_or_else (défaut), ok_or (donne une raison à un None), map_err (enrichis l'erreur). Et si tu veux vraiment paniquer, .expect("message") au moins explique pourquoi l'invariant comptait — un crash documenté vaut mieux qu'un unwrap muet.Before writing .unwrap(), run through: ? (propagate), unwrap_or / unwrap_or_else (default), ok_or (give a None a reason), map_err (enrich the error). And if you truly want to panic, .expect("message") at least explains why the invariant mattered — a documented crash beats a mute unwrap.

Le test mentalThe mental test

« Si ce .unwrap() panique en production, est-ce parce qu'un bug de MON code a brisé une promesse — ou parce que le monde extérieur s'est comporté normalement ? » Si c'est le monde extérieur (fichier, réseau, saisie), unwrap est une faute : propage. Si c'est une promesse interne que rien d'externe ne peut violer, la panique est défendable — c'est le sujet du n°3.'If this .unwrap() panics in production, is it because a bug in MY code broke a promise — or because the outside world behaved normally?' If it's the outside world (file, network, input), unwrap is a mistake: propagate. If it's an internal promise nothing external can violate, the panic is defensible — that's issue n°3's subject.

Un .unwrap() sur une erreur attendue ne supprime pas l'échec : il le déguise en crash et le repousse jusqu'à l'utilisateur.An .unwrap() on an expected error doesn't remove the failure: it disguises it as a crash and pushes it all the way to the user.
№ 05Bilan & éditoWrap-up & editorial2 min

L'erreur dans le type, et le compilateur qui t'oblige à la regarder.The error in the type, and the compiler that makes you look at it.

Ce numéro a posé la fondation du Vol 4 : l'échec attendu n'est ni une exception qui saute par-dessus ton code, ni un crash qu'on déclenche pour faire taire le compilateur. C'est une valeur — Result<T, E> — qui vit dans la signature, que le compilateur t'oblige à traiter, et que l'opérateur ? laisse propager sans bruit. Reste la première question, celle qui décide de tout : cet échec est-il attendu, ou est-ce un bug ? Y répondre juste, c'est savoir quoi mettre en Result, et quoi laisser paniquer.This issue laid Vol 4's foundation: an expected failure is neither an exception leaping over your code, nor a crash you trigger to silence the compiler. It's a value — Result<T, E> — that lives in the signature, that the compiler makes you handle, and that the ? operator lets you propagate quietly. What remains is the first question, the one that decides everything: is this failure expected, or is it a bug? Answering it well is knowing what to put in a Result, and what to let panic.

L'outilThe toolCe qu'il rend possibleWhat it makes possibleLe réflexeThe reflex
Result<T, E>L'échec dans le type de retour, visible et imposé.Failure in the return type, visible and enforced.Peut échouer normalement → Result, pas une exception cachée.Can fail normally → Result, not a hidden exception.
?La propagation légère, mais explicite et typée.Light propagation, but explicit and typed.« L'appelant décide » → ?, qui convertit l'erreur via From.'The caller decides' → ?, converting the error via From.
CombinateursCombinatorsTransformer un Result sans le déballer (map, ok_or…).Transforming a Result without unwrapping (map, ok_or…).Réponse locale à l'échec → combinateur, pas de match nu.Local answer to failure → combinator, no bare match.
.unwrap()Extraire en pariant qu'il n'y a pas d'Err — au prix d'un crash.Extract betting there's no Err — at the cost of a crash.Jamais sur une erreur attendue ; réservé aux invariants (n°3).Never on an expected error; reserved for invariants (n°3).
Gérer une erreur en Rust commence par une question, pas par un try : est-ce attendu, ou est-ce un bug — et le type qu'on choisit en découle.Handling an error in Rust starts with a question, not a try: is this expected, or is it a bug — and the type you choose follows.
Mot de l'éditeurFrom the editor

Tu sais signaler un échec. Reste à lui faire dire ce qui a mal tourné.You know how to signal a failure. Now to make it say what went wrong.

Ce numéro t'a donné le contenant : Result, et le ? pour le faire circuler. Mais dans nos exemples, le E est resté vague — une AppError dont on n'a jamais montré la forme. Or un échec utile ne dit pas seulement « ça a raté » : il dit quoi, où, et parfois pourquoi. Un Err sans information, c'est un None déguisé.This issue gave you the container: Result, and the ? to move it around. But in our examples, the E stayed vague — an AppError whose shape we never showed. Yet a useful failure doesn't just say 'it failed': it says what, where, and sometimes why. An Err with no information is a None in disguise.

Vol 4 №2 : concevoir ses erreurs. On dessine le type E lui-même — un enum qui énumère les façons d'échouer, des conversions From pour que ? les unifie, et le grand partage : une bibliothèque, qui doit laisser l'appelant distinguer chaque cas (le réflexe thiserror), contre une application, qui veut surtout un contexte lisible et une seule erreur fourre-tout (le réflexe anyhow). Le même enum, encore — cette fois au service de la clarté du diagnostic.Vol 4 №2: designing your errors. We draw the E type itself — an enum that enumerates the ways to fail, From conversions so ? unifies them, and the great divide: a library, which must let the caller tell each case apart (the thiserror reflex), versus an application, which mostly wants readable context and one catch-all error (the anyhow reflex). The same enum, again — this time in service of a clear diagnosis.