Vol. 3 · № 02 · Un atelier RustA Rust workshop

Option, ou l'absence de null Option, or the absence of null

Le milliard de dollars qu'on ne paie pas : l'absence devient un cas qu'on DOIT traiter. unwrap et expect ont un prix — on le nomme. The billion-dollar mistake you don't pay: absence becomes a case you MUST handle. unwrap and expect have a price — we name it.

AudienceAudience
Dev ayant vu enums & pattern matching (Vol 3 №1) Dev who has seen enums & pattern matching (Vol 3 №1)
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●○○ TypesOptionnull
SommaireContents ·
№ 01CadrageFraming3 min

L'absence n'est plus un piège caché. C'est un cas dans le type.Absence is no longer a hidden trap. It's a case in the type.

Le numéro précédent s'est clos sur le « bug à un milliard de dollars » : le null, cette valeur qui prétend être de n'importe quel type sans en être aucun. Rust ne l'a jamais introduit. À la place, un simple enum à deux variantes — Some(T) ou None — qui transforme « cette donnée pourrait être absente » d'une note dans ta tête en une contrainte que le compilateur fait respecter. C'est l'enum du numéro précédent, appliqué au problème le plus universel de la programmation.The previous issue closed on the 'billion-dollar mistake': null, that value pretending to be of any type while being none. Rust never introduced it. In its place, a plain two-variant enum — Some(T) or None — turning 'this data might be absent' from a note in your head into a constraint the compiler enforces. It's the previous issue's enum, applied to the most universal problem in programming.

Option n'est pas un mot-clé : c'est un enum ordinaireOption isn't a keyword: it's an ordinary enum
enum Option<T> {     // dans la bibliothèque standard, rien de magique :
    Some(T),         // il y a une valeur, la voici
    None,            // il n'y en a pas
}

// un type qui PEUT être absent le dit dans sa signature :
fn trouver(id: u32) -> Option<Utilisateur> { /* ... */ }
//                     ^^^^^^^^^^^^^^^^^^^^ « peut-être un User, peut-être rien »
La différence radicale avec nullThe radical difference from null

Avec le null, toute référence peut secrètement être nulle — le type Utilisateur ment, il signifie en réalité « Utilisateur, ou bien rien, on verra à l'exécution ». Avec Option, le type dit la vérité : Utilisateur est toujours un utilisateur, et Option<Utilisateur> est le seul à pouvoir être absent. L'absence n'est plus partout par défaut : elle est exactement là où le type l'écrit, et nulle part ailleurs.With null, any reference can secretly be null — the type Utilisateur lies, it actually means 'Utilisateur, or else nothing, we'll see at runtime'. With Option, the type tells the truth: Utilisateur is always a user, and only Option<Utilisateur> can be absent. Absence is no longer everywhere by default: it's exactly where the type writes it, and nowhere else.

L'optimisation de niche — None ne coûte pas un octet de plusThe niche optimization — None costs not a byte more
&T — 8 octets, jamais 08 bytes, never 0
0x7ffe_1a40une adresse réellea real address
Option<&T> — 8 octets aussi8 bytes too
0x7ffe_1a40Some(&T) — l'adressethe address
0x0000_0000None — le 0 interdit, réutiliséthe forbidden 0, reused
Le réflexe du numéroThe issue's reflex

« Cette valeur peut-elle légitimement ne pas exister ? » Si oui — un utilisateur introuvable, un champ facultatif, le premier élément d'une liste peut-être vide — alors son type est Option<T>. Et dès lors, le compilateur t'interdit de lire la valeur sans avoir d'abord traité le cas où elle manque.'Can this value legitimately not exist?' If yes — a user not found, an optional field, the first element of a maybe-empty list — then its type is Option<T>. And from then on, the compiler forbids you from reading the value without first handling the case where it's missing.

Aucun coût caché.No hidden cost.

On pourrait croire qu'envelopper chaque valeur facultative dans un enum coûte de la mémoire ou des indirections. Pour les références et beaucoup de types, non : le compilateur sait qu'une référence n'est jamais l'adresse zéro, et code None par ce zéro. Option<&T> occupe exactement la taille d'un pointeur — l'« optimisation de niche ». La sécurité est ici gratuite, au sens littéral.You might think wrapping each optional value in an enum costs memory or indirection. For references and many types, no: the compiler knows a reference is never address zero, and encodes None as that zero. Option<&T> takes exactly the size of a pointer — the 'niche optimization'. Safety here is free, in the literal sense.

№ 02La surpriseThe surprise4 min

Le crash qui guettait à l'exécution devient une erreur de compilation.The crash that lurked at runtime becomes a compile error.

Dans un langage à null, accéder à une valeur absente ne se voit jamais à l'écriture : le code compile, paraît correct, et explose en production sur la première donnée manquante. La NullPointerException est l'archétype du bug qui passe tous les tests et tombe chez l'utilisateur. Avec Option, ce même accès ne compile pas — l'erreur a remonté du runtime jusqu'au clavier, là où elle coûte le moins cher à corriger.In a null language, accessing an absent value never shows at writing time: the code compiles, looks fine, and blows up in production on the first missing datum. The NullPointerException is the archetype of the bug that passes every test and lands on the user. With Option, that same access won't compile — the error has moved up from runtime to keyboard, where it's cheapest to fix.

Le null — rien ne distingue l'accès sûr du crashnull — nothing tells the safe access from the crash
// Java / TypeScript : le type ne dit rien, le crash attend
Utilisateur u = trouver(id);     // peut être null — rien ne le signale
System.out.println(u.nom);       // 💥 NullPointerException à l'exécution

// la seule défense est la discipline : se SOUVENIR de tester
if (u != null) { ... }           // si on oublie, le compilateur se tait

Le type Utilisateur ne fait aucune différence entre « toujours là » et « peut-être null ». La seule barrière est ta mémoire : penser à tester avant d'accéder. Le compilateur, lui, ne dit rien — il laisse passer l'accès direct comme le test prudent. Des décennies de NPE viennent exactement de là : un type qui ment sur ce qu'il peut contenir, et une vérification qui dépend de la vigilance humaine.The type Utilisateur makes no difference between 'always there' and 'maybe null'. The only barrier is your memory: remembering to test before access. The compiler says nothing — it lets the direct access through just like the careful test. Decades of NPEs come exactly from this: a type lying about what it can hold, and a check that depends on human vigilance.

Le null te fait confiance pour te souvenir. Option ne te fait pas confiance — et c'est pour ça qu'il ne plante jamais par surprise.null trusts you to remember. Option doesn't trust you — and that's why it never crashes by surprise.
№ 03La mécaniqueThe mechanics5 min

Ouvrir un Option sans noyer le code sous les match.Opening an Option without drowning the code in match.

Si chaque valeur facultative imposait un match complet, le code deviendrait vite illisible. Heureusement, Option vient avec une boîte à outils d'extraction : if let pour le cas unique, une famille de combinateurs pour transformer sans déballer, et l'opérateur ? pour propager l'absence en une touche. On choisit selon ce qu'on veut faire de None — fournir un défaut, transformer, enchaîner, ou abandonner tôt.If every optional value demanded a full match, code would quickly turn unreadable. Happily, Option ships with an extraction toolbox: if let for the single case, a family of combinators to transform without unwrapping, and the ? operator to propagate absence in one keystroke. You pick based on what you want to do with None — supply a default, transform, chain, or bail early.

if let — quand un seul cas t'intéresseif let — when only one case matters
if let Some(u) = trouver(id) {
    println!("Trouvé : {}", u.nom);   // ne s'exécute que si Some
}                                     // le cas None est implicitement ignoré

Les combinateurs — transformer sans déballerCombinators — transform without unwrapping

.unwrap_or(def)renvoie la valeur, ou def si Nonereturns the value, or def if None
.map(|v| …)transforme la valeur si présente, sinon laisse Nonetransforms the value if present, else leaves None
.and_then(|v| …)enchaîne une autre opération qui renvoie elle-même un Optionchains another operation that itself returns an Option
?extrait la valeur, ou retourne None de la fonction immédiatementextracts the value, or returns None from the function at once

L'opérateur ? — propager l'absence en une toucheThe ? operator — propagate absence in one keystroke

// Sans Option : une cascade de if/null imbriqués, fragile
// Avec : une chaîne plate, chaque maillon court-circuite sur None

fn ville_du_chef(equipe: &Equipe) -> Option<String> {
    let chef = equipe.chef.as_ref()?;         // None ? on sort, renvoie None
    let adresse = chef.adresse.as_ref()?;     // pareil
    Some(adresse.ville.clone())               // arrivé ici, tout était présent
}
// as_ref() : on n'a qu'un &Equipe — sans lui, le ? tenterait de DÉPLACER
// hors d'un emprunt partagé (error[E0507]). Il rend Option<&T> au lieu d'Option<T>.

// ou en style combinateurs :
equipe.chef.as_ref()
    .and_then(|c| c.adresse.as_ref())
    .map(|a| a.ville.clone())

Le ? dit : « si c'est Some, donne-moi la valeur et continue ; si c'est None, arrête la fonction et renvoie None ». Toute la cascade de vérifications imbriquées qu'on écrirait avec des if null devient une suite plate de maillons, où l'absence court-circuite proprement. Le type suit la présence à travers toute la chaîne — et on ne lit jamais une valeur qui n'a pas survécu à chaque étape.The ? says: 'if it's Some, hand me the value and continue; if it's None, stop the function and return None'. The whole cascade of nested checks you'd write with if null becomes a flat chain of links, where absence short-circuits cleanly. The type tracks presence through the entire chain — and you never read a value that didn't survive every step.

Traiter l'absence n'oblige pas à l'écrire à la main partout. Le ? et les combinateurs la font traverser le code sans bruit.Handling absence needn't mean writing it by hand everywhere. The ? and the combinators carry it through the code without noise.
№ 04Le piègeThe trap4 min

.unwrap() : rouvrir la porte que Option venait de fermer..unwrap(): reopening the door Option just closed.

Option force à traiter l'absence — alors le débutant pressé cherche le bouton qui fait taire cette obligation. Il le trouve : .unwrap() extrait la valeur du Some… et panique si c'est None. En une méthode, on a réintroduit exactement le crash à l'exécution que tout le type servait à éliminer. unwrap n'est pas interdit, mais il a un prix : il échange une garantie de compilation contre un pari sur l'exécution. Ce prix, il faut le nommer pour le payer en conscience.Option forces you to handle absence — so the hurried beginner looks for the button that silences the obligation. They find it: .unwrap() extracts the value from Some… and panics if it's None. In one method, you've reintroduced exactly the runtime crash the whole type existed to eliminate. unwrap isn't forbidden, but it has a price: it trades a compile-time guarantee for a runtime gamble. That price must be named to be paid knowingly.

Le contre-exemple — unwrap, le null qui revient par la fenêtreThe counter-example — unwrap, the null coming back through the window
let u = trouver(id).unwrap();    // « je SAIS qu'il existe »
println!("{}", u.nom);           // …jusqu'au jour où il n'existe pas :
// thread 'main' panicked at src/main.rs:1:23:
// called `Option::unwrap()` on a `None` value

// .unwrap() rouvre exactement la porte que Option avait fermée :
// le crash à l'exécution sur une valeur absente. Le null, déguisé.

« Je sais qu'il existe » est une affirmation sur l'exécution que le compilateur ne peut pas vérifier — exactement le genre de pari qui tient en développement et casse en production. Chaque .unwrap() est un endroit où tu as repris à ta charge la garantie que le type t'offrait. Parfois c'est justifié ; souvent c'est juste le réflexe de faire taire l'erreur. Un .unwrap() non réfléchi est un null deref qui attend sa date.'I know it exists' is a claim about runtime the compiler can't verify — exactly the kind of bet that holds in development and breaks in production. Each .unwrap() is a place where you took back the guarantee the type was giving you. Sometimes it's justified; often it's just the reflex of silencing the error. A thoughtless .unwrap() is a null deref waiting for its date.

Le bon réflexe — propager, défaut, ou prouver l'invariantThe right reflex — propagate, default, or prove the invariant
// 1. Propager : « ce n'est pas à moi de décider quoi faire de l'absence »
let u = trouver(id)?;                       // renvoie None/Err à l'appelant

// 2. Fournir un défaut sensé
let nom = trouver(id).map(|u| u.nom).unwrap_or("anonyme".into());

// 3. Si l'absence est VRAIMENT impossible, le dire — et le prouver
let premier = nums.first()
    .expect("nums est non-vide : garanti par la validation en amont");
//   ^^^^^^ le message documente l'invariant qui rend None impossible

Trois sorties honnêtes, par ordre de préférence : propager avec ? quand la décision revient à l'appelant ; fournir un défaut quand l'absence a un sens métier ; et, seulement si None est réellement impossible, .expect() avec un message qui documente l'invariant. La différence entre .unwrap() et .expect("…") n'est pas cosmétique : le message force à écrire pourquoi ça ne peut pas arriver — et si tu n'arrives pas à l'écrire, c'est que tu n'en sais rien.Three honest exits, in order of preference: propagate with ? when the decision belongs to the caller; supply a default when absence has a domain meaning; and, only if None is genuinely impossible, .expect() with a message that documents the invariant. The difference between .unwrap() and .expect("…") isn't cosmetic: the message forces you to write why it can't happen — and if you can't write it, you don't actually know.

Quand unwrap est légitimeWhen unwrap is legitimate

Les tests (où une panique est l'échec attendu), les prototypes jetables, et les cas où un invariant local rend None vraiment impossible et prouvable en une phrase. Dans ces cas, préfère .expect() à .unwrap() : le message est la preuve, laissée pour le prochain lecteur — toi dans six mois compris.Tests (where a panic is the expected failure), throwaway prototypes, and cases where a local invariant makes None truly impossible and provable in one sentence. In those, prefer .expect() over .unwrap(): the message is the proof, left for the next reader — including you in six months.

Le test mentalThe mental test

Avant chaque .unwrap() : « suis-je capable d'écrire, en une phrase, pourquoi None est impossible ici ? » Si oui, écris-la dans un .expect() — c'est gratuit et ça documente. Si non, tu n'as pas le droit de l'affirmer : propage avec ?, ou fournis un défaut. Le code de production ne devrait presque jamais contenir un .unwrap() nu.Before each .unwrap(): 'can I write, in one sentence, why None is impossible here?' If yes, write it in an .expect() — it's free and it documents. If not, you have no right to assert it: propagate with ?, or supply a default. Production code should almost never hold a bare .unwrap().

.unwrap() ne supprime pas l'absence : il parie qu'elle n'arrivera pas, et te fait crasher s'il perd..unwrap() doesn't remove absence: it bets it won't happen, and crashes you if it loses.
№ 05Bilan & éditoWrap-up & editorial2 min

Un milliard de dollars économisés par un enum à deux variantes.A billion dollars saved by a two-variant enum.

Option n'est rien d'autre que l'enum du numéro précédent, appliqué à l'absence : Some(T) ou None. Mais ce rien-d'autre supprime une catégorie entière de crashes. Le type dit la vérité sur ce qui peut manquer ; le compilateur t'oblige à traiter le manque avant de lire ; et les outils — match, if let, ?, combinateurs — rendent ce traitement léger. Le seul moyen de retrouver le crash est de le redemander explicitement, par unwrap.Option is nothing but the previous issue's enum, applied to absence: Some(T) or None. But that nothing-but removes an entire class of crashes. The type tells the truth about what can be missing; the compiler forces you to handle the miss before reading; and the tools — match, if let, ?, combinators — make that handling light. The only way to get the crash back is to ask for it explicitly, via unwrap.

L'outilThe toolQuand l'employerWhen to use it
match / if letTraiter explicitement Some et None — la base, quand les deux cas comptent.Handle Some and None explicitly — the baseline, when both cases matter.
.unwrap_or(d) / .map()Fournir un défaut, ou transformer la valeur sans la déballer.Supply a default, or transform the value without unwrapping it.
? / .and_then()Propager l'absence le long d'une chaîne, sans imbrication.Propagate absence along a chain, without nesting.
.expect("…")None vraiment impossible : le message documente et prouve l'invariant.None genuinely impossible: the message documents and proves the invariant.
.unwrap() nubareLe pari sur l'exécution : à fuir en production. Le null déguisé.The runtime gamble: avoid in production. Null in disguise.
Option ne rend pas l'absence rare : il la rend impossible à ignorer. Le crash ne revient que si tu le rappelles par son nom.Option doesn't make absence rare: it makes it impossible to ignore. The crash returns only if you call it back by name.
Mot de l'éditeurFrom the editor

L'absence n'était qu'un invariant. Il y en a mille autres.Absence was just one invariant. There are a thousand more.

Option résout un cas si universel qu'il est dans la bibliothèque standard — « cette valeur peut manquer ». Mais regarde ce qu'il a vraiment fait : il a pris une règle qui vivait dans ta tête (« attention, ça peut être null ») et l'a déplacée dans le type, là où le compilateur la fait respecter. Une fois qu'on a vu ce mouvement, on ne peut plus le ne pas voir. « Cette chaîne doit être un email validé. » « Cette connexion doit être ouverte avant qu'on envoie. » « Cette quantité ne peut pas être négative. » Toutes ces règles, aujourd'hui, vivent dans des commentaires et des tests. Et si elles vivaient dans les types ?Option solves a case so universal it's in the standard library — 'this value may be missing'. But look at what it really did: it took a rule that lived in your head ('careful, this may be null') and moved it into the type, where the compiler enforces it. Once you've seen that move, you can't unsee it. 'This string must be a validated email.' 'This connection must be open before we send.' 'This quantity can't be negative.' All these rules, today, live in comments and tests. What if they lived in the types?

Prochain numéro, et fin du Vol 3 : newtype & type-state. Comment encoder tes propres invariants dans des types à toi — un Email qu'on ne peut construire que validé, une Connection<Open> sur laquelle send() existe et une Connection<Closed> sur laquelle il n'existe tout simplement pas. L'état illégal qui ne compile pas, étendu de la bibliothèque standard à ton domaine métier. Avec, en contre-exemple, la cérémonie de types qui coûte plus cher que le bug qu'elle prévient.Next issue, and the close of Vol 3: newtype & type-state. How to encode your own invariants into your own types — an Email you can only build validated, a Connection<Open> on which send() exists and a Connection<Closed> on which it simply doesn't. The illegal state that won't compile, extended from the standard library to your domain. With, as a counter-example, the type ceremony that costs more than the bug it prevents.