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

Concevoir ses erreurs : thiserror vs anyhow Designing your errors: thiserror vs anyhow

Un type d'erreur qui dit ce qui a échoué — et le choix bibliothèque vs application : enum d'erreur précis et From pour une lib, contexte agrégé pour une app. An error type that says what failed — and the library vs application choice: a precise error enum and From for a lib, aggregated context for an app.

AudienceAudience
Dev maîtrisant Result et l'opérateur ? (Vol 4 №1) Dev fluent in Result and the ? operator (Vol 4 №1)
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●○ Erreursthiserroranyhow
SommaireContents ·
№ 01CadrageFraming3 min

Un Err qui ne dit rien, c'est un None déguisé.An Err that says nothing is a None in disguise.

Le numéro précédent a donné le contenant — Result, et le ? pour le faire circuler. Mais le E est resté vague. Or le pouvoir de Result vient justement de ce que l'échec transporte une information : sans elle, on retombe au niveau d'Option, où il n'y a qu'un « ça n'a pas marché ». Une erreur utile dit trois choses : ce qui a échoué (la cause), avec quel contexte (le fichier, la ligne, l'identifiant), et de manière que l'appelant puisse soit réagir au cas près, soit l'afficher proprement. Concevoir ses erreurs, c'est dessiner ce type E — et le premier réflexe est d'en faire un enum, une variante par façon d'échouer.The previous issue gave the container — Result, and the ? to move it around. But the E stayed vague. Yet Result's power comes precisely from failure carrying information: without it, you fall back to Option's level, where there's only a 'it didn't work'. A useful error says three things: what failed (the cause), with what context (the file, the line, the id), and in a way that lets the caller either react case by case or display it cleanly. Designing your errors means drawing that E type — and the first reflex is to make it an enum, one variant per way to fail.

L'erreur comme enum — une variante par cause, l'appelant peut trierThe error as an enum — one variant per cause, the caller can sort
// Un type d'erreur qui DIT ce qui a échoué — une variante par cause
enum ChargementError {
    FichierAbsent(PathBuf),       // on sait QUEL fichier manquait
    TomlInvalide(toml::Error),    // on garde la cause d'origine
    PortManquant,                 // une condition métier nommée
}

// l'appelant peut réagir au cas près :
match charger_config() {
    Err(ChargementError::FichierAbsent(p)) => creer_defaut(&p),  // récupérable
    Err(e) => return Err(e),                                     // le reste remonte
    Ok(c)  => c,
}
Réagir, ou afficherReact, or display

Il y a deux choses qu'on veut faire d'une erreur, et elles tirent le design dans deux directions. Réagir : reprendre sur tel cas précis (recréer un fichier absent, retenter sur un timeout) — ça exige un type où chaque cause est distinguable, donc un enum qu'on peut match. Afficher : présenter un message clair, avec le contexte de ce qu'on tentait — ça exige surtout une bonne chaîne de causes, pas forcément des variantes typées. Tout le numéro tient dans cet arbitrage, qui culminera au chapitre 4 en « bibliothèque vs application ».There are two things you want to do with an error, and they pull the design in two directions. React: resume on a precise case (recreate a missing file, retry on a timeout) — that needs a type where each cause is distinguishable, hence an enum you can match. Display: present a clear message, with context of what you were attempting — that mostly needs a good cause chain, not necessarily typed variants. The whole issue lives in this trade-off, peaking in this issue's 'library vs application'.

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

« Quelqu'un voudra-t-il réagir à tel échec précis, ou juste savoir que ça a raté ? » Si l'appelant doit pouvoir trier les causes, donne-lui un enum où chacune est une variante. S'il ne fera qu'afficher, un type d'erreur agrégé, riche en contexte, suffit. La forme du type d'erreur découle de ce que son consommateur en fera.'Will someone want to react to this precise failure, or just know it failed?' If the caller must sort the causes, give them an enum where each is a variant. If they'll only display it, an aggregated, context-rich error type is enough. The error type's shape follows from what its consumer will do with it.

String n'est pas un type d'erreur.String is not an error type.

Renvoyer Err(format!("fichier absent…")) compile et semble pratique. Mais l'appelant ne reçoit qu'une phrase : il ne peut pas distinguer « fichier absent » de « port manquant » sans parser du texte. Une erreur en String, c'est jeter l'information au moment même où on la produit — l'enum la garde structurée jusqu'au point où quelqu'un décidera quoi en faire.Returning Err(format!("missing file…")) compiles and feels handy. But the caller gets only a sentence: they can't tell 'missing file' from 'missing port' without parsing text. A String error throws the information away at the very moment you produce it — the enum keeps it structured up to the point where someone decides what to do with it.

№ 02La surpriseThe surprise4 min

Une erreur en texte se lit. Une erreur en type se traite.A text error gets read. A typed error gets handled.

La voie de la facilité, c'est l'erreur en String : un message formaté, renvoyé tel quel. Ça compile, ça s'affiche, et tant qu'on ne fait qu'afficher, ça suffit presque. Le problème surgit dès qu'on veut réagir : reprendre sur tel cas, retenter sur tel autre, ignorer le troisième. Avec un message texte, la seule façon de distinguer les causes est d'en parser le contenu — un couplage fragile qui casse à la première reformulation. L'enum d'erreur garde chaque cause comme une variante distincte : l'appelant match dessus, le compilateur vérifie l'exhaustivité, et le message d'affichage devient une projection du type, pas sa seule réalité.The easy path is the String error: a formatted message, returned as-is. It compiles, it displays, and as long as you only display, it almost suffices. The problem arises the moment you want to react: resume on this case, retry on that one, ignore the third. With a text message, the only way to tell causes apart is to parse its content — a fragile coupling that breaks at the first rewording. The error enum keeps each cause as a distinct variant: the caller matches on it, the compiler checks exhaustiveness, and the display message becomes a projection of the type, not its only reality.

Le contre-exemple — l'information jetée au moment où on la produitThe counter-example — information thrown away as it's produced
// L'erreur « stringly typed » — un message, et rien d'autre
fn charger(chemin: &str) -> Result<Config, String> {
    let texte = std::fs::read_to_string(chemin)
        .map_err(|e| format!("lecture impossible: {e}"))?;
    toml::from_str(&texte)
        .map_err(|e| format!("toml invalide: {e}"))
}

// l'appelant ne peut que l'AFFICHER. Pour réagir « si le fichier manque,
// crée-le », il devrait fouiller la chaîne — fragile, et cassé à la
// première reformulation du message.

Chaque map_err écrase une erreur riche — une io::Error qui sait si le fichier manquait ou si l'accès était refusé — en une simple phrase. L'appelant hérite d'un String : il peut le logger, pas raisonner dessus. Le jour où il veut « recréer le fichier s'il est absent », il doit chercher un bout de texte dans le message, et son code casse dès qu'on retouche la formulation. On a transformé une donnée structurée en prose, irréversiblement.Each map_err crushes a rich error — an io::Error that knows whether the file was missing or access denied — into a plain sentence. The caller inherits a String: they can log it, not reason about it. The day they want to 'recreate the file if it's missing', they must search for a snippet of text in the message, and their code breaks the moment you tweak the wording. You turned structured data into prose, irreversibly.

Mettre une erreur en String, c'est décider à la place de l'appelant qu'il ne fera jamais que l'afficher.Putting an error in a String is deciding for the caller that they'll never do more than display it.
№ 03La mécaniqueThe mechanics5 min

thiserror : l'enum d'erreur, sans le boilerplate de l'enum d'erreur.thiserror: the error enum, without the error enum's boilerplate.

Un bon type d'erreur doit faire trois choses : s'afficher lisiblement (implémenter Display), s'intégrer à l'écosystème (implémenter le trait Error), et se laisser produire facilement par ? (avoir des From depuis les erreurs sous-jacentes). À la main, c'est trois impl par variante — un mur de code répétitif qui décourage de bien typer ses erreurs. La crate thiserror génère tout cela depuis des annotations. On décrit l'enum et ses messages ; la macro dérive Display, Error, et les From marqués #[from]. Le type d'erreur précis redevient bon marché — et donc, enfin, celui qu'on écrit par défaut.A good error type must do three things: display readably (implement Display), integrate with the ecosystem (implement the Error trait), and be easy to produce via ? (have From conversions from the underlying errors). By hand, that's three impls per variant — a wall of repetitive code that discourages typing your errors well. The thiserror crate generates all of it from annotations. You describe the enum and its messages; the macro derives Display, Error, and the From conversions marked #[from]. The precise error type becomes cheap again — and so, at last, the one you write by default.

L'enum annoté — Display et From dérivés par la macroThe annotated enum — Display and From derived by the macro
use thiserror::Error;

#[derive(Debug, Error)]
enum ChargementError {
    #[error("lecture impossible: {0}")]      // ce que verra l'humain (Display)
    Lecture(#[from] std::io::Error),          // #[from] : conversion automatique

    #[error("toml invalide: {0}")]
    Toml(#[from] toml::Error),

    #[error("le champ 'port' est absent")]
    PortManquant,                             // une variante sans cause source
}
io::Error
──#[from]──→
toml::Error
──#[from]──→
PortManquant
──variante directedirect variant──→
ChargementError
un seul type, que ? produitone type, that ? produces
#[from] + ? = la propagation gratuite#[from] + ? = free propagation
fn charger(chemin: &str) -> Result<Config, ChargementError> {
    let texte = std::fs::read_to_string(chemin)?;   // io::Error -> ChargementError
    let config: Config = toml::from_str(&texte)?;   // toml::Error -> ChargementError
    config.port.ok_or(ChargementError::PortManquant)?;
    Ok(config)
}
// plus un seul map_err : #[from] a généré les From, le ? les appelle.
// l'enum reste matchable par l'appelant, ET s'affiche proprement.

C'est ici que les deux numéros se rejoignent. Le n°1 a montré que ? appelle From pour convertir l'erreur au passage ; #[from] est précisément ce qui fournit ce From. Une fois les variantes sources marquées, chaque ? sait convertir l'erreur sous-jacente vers ton enum, sans map_err. Tu obtiens la lisibilité du chemin heureux et un type d'erreur que l'appelant peut inspecter — les deux à la fois, enfin sans rançon de boilerplate.This is where the two issues meet. Issue n°1 showed that ? calls From to convert the error on the way; #[from] is precisely what provides that From. Once the source variants are marked, each ? knows how to convert the underlying error into your enum, with no map_err. You get the happy path's readability and an error type the caller can inspect — both at once, finally without a boilerplate ransom.

source, pour la chaîne de causessource, for the cause chain

Une variante marquée #[from] (ou #[source]) devient la cause de l'erreur au sens du trait Error : err.source() remonte la chaîne. C'est ce qui permet aux outils d'affichage de montrer « toml invalide → ligne 4 : attendu un entier » — l'erreur de surface et son origine. Une bonne erreur n'efface pas celle qui l'a causée ; elle l'emboîte.A variant marked #[from] (or #[source]) becomes the error's cause in the Error trait's sense: err.source() walks the chain. That's what lets display tools show 'invalid toml → line 4: expected an integer' — the surface error and its origin. A good error doesn't erase the one that caused it; it nests it.

Une macro, pas de la magie.A macro, not magic.

thiserror ne fait rien que tu ne pourrais écrire à la main : c'est une macro dérive qui génère les impl Display, Error et From à ta place, à la compilation. Zéro coût à l'exécution, zéro dépendance dans ton API publique (l'enum est à toi). C'est de l'automatisation de boilerplate, exactement comme #[derive(Debug)] — pas une abstraction qui s'interpose entre toi et le type.thiserror does nothing you couldn't write by hand: it's a derive macro that generates the Display, Error and From impls for you, at compile time. Zero runtime cost, zero dependency in your public API (the enum is yours). It's boilerplate automation, exactly like #[derive(Debug)] — not an abstraction that wedges itself between you and the type.

Bien typer ses erreurs n'est plus un luxe coûteux : une annotation par variante, et la macro écrit le reste.Typing your errors well is no longer a costly luxury: one annotation per variant, and the macro writes the rest.
№ 04Le piègeThe trap4 min

Le grand partage : une bibliothèque laisse réagir, une application rapporte.The great divide: a library lets you react, an application reports.

Deux crates dominent la gestion d'erreur, et le réflexe « laquelle ? » n'a qu'une bonne réponse : ça dépend de qui appelle. thiserror produit un enum précis, matchable — c'est l'outil d'une bibliothèque, parce que ses consommateurs sont inconnus et voudront peut-être réagir à tel cas. anyhow produit une erreur unique et opaque, qui agrège n'importe quelle source et empile du contexte — c'est l'outil d'une application, d'un binaire au sommet de la pile, où l'on ne « réagit » presque jamais et où l'on veut surtout un rapport d'échec lisible. Le piège n'est pas de choisir l'un ou l'autre, mais de les inverser.Two crates dominate error handling, and the reflex 'which one?' has only one good answer: it depends on who calls. thiserror produces a precise, matchable enum — it's a library's tool, because its consumers are unknown and may want to react to a given case. anyhow produces a single, opaque error that aggregates any source and stacks context — it's an application's tool, a binary at the top of the stack, where you almost never 'react' and mostly want a readable failure report. The trap isn't choosing one or the other, but swapping them.

Le contre-exemple — anyhow dans une bibliothèqueThe counter-example — anyhow in a library
// Une BIBLIOTHÈQUE qui renvoie anyhow::Error
use anyhow::Context;   // .context() vient du trait, pas de l'erreur

pub fn parse_facture(src: &str) -> anyhow::Result<Facture> {
    let doc = parse_xml(src).context("xml malformé")?;
    let total = doc.total().context("total manquant")?;
    Ok(Facture { total })
}

// l'appelant de ta lib reçoit un anyhow::Error : une erreur opaque,
// agrégée. Il peut l'afficher, pas la MATCHER. Impossible d'écrire
// « si la facture n'a pas de total, demande-le à l'utilisateur ».
// tu as imposé À TOUS TES CONSOMMATEURS de ne jamais réagir.

En renvoyant anyhow::Error depuis une bibliothèque, tu prives tous tes appelants de la capacité de distinguer les causes. anyhow efface le type concret derrière une erreur unique : parfait quand on ne fera qu'afficher, désastreux quand quelqu'un, en aval, aurait voulu reprendre sur un cas précis. Une lib ne connaît pas ses consommateurs ; elle ne doit donc pas décider à leur place qu'ils ne réagiront jamais.By returning anyhow::Error from a library, you deprive all your callers of the ability to tell causes apart. anyhow erases the concrete type behind a single error: perfect when you'll only display, disastrous when someone downstream would have wanted to resume on a precise case. A library doesn't know its consumers; so it must not decide for them that they'll never react.

Le bon réflexe — enum précis pour la lib, agrégat pour le binaireThe right reflex — precise enum for the lib, aggregate for the binary
// La LIB expose un enum précis (thiserror) — l'appelant peut trier
#[derive(Debug, thiserror::Error)]
pub enum FactureError {
    #[error("xml malformé: {0}")]
    Xml(#[from] XmlError),
    #[error("total manquant")]
    TotalManquant,
}
pub fn parse_facture(src: &str) -> Result<Facture, FactureError> { /* ... */ }

// L'APPLICATION (le binaire) agrège tout en anyhow + du contexte
fn main() -> anyhow::Result<()> {
    let src = std::fs::read_to_string("facture.xml")?;
    let f = parse_facture(&src).context("chargement de la facture")?;
    println!("{}", f.total);
    Ok(())   // ici personne ne « réagit » : on veut un rapport lisible.
}

La bibliothèque expose un enum thiserror : ses consommateurs peuvent match. L'application, elle, se sert d'anyhow pour agréger toutes ces erreurs précises en une seule, et empiler du contexte (.context("…")) à chaque étage — ce qui produira un rapport « chargement de la facture → total manquant » exploitable par un humain. Chaque outil à sa place : la lib préserve l'information, l'app la met en forme.The library exposes a thiserror enum: its consumers can match. The application, in turn, uses anyhow to aggregate all those precise errors into one, and stack context (.context("…")) at each level — yielding a human-usable report 'loading the invoice → missing total'. Each tool in its place: the lib preserves the information, the app shapes it.

La règle de décisionThe decision rule

« Mon code est-il appelé par d'autres que moi, qui pourraient vouloir réagir à un échec précis ? » Oui → bibliothèque → thiserror, un enum que l'appelant peut matcher. Non, je suis au sommet de la pile et je ne fais que rapporter → application → anyhow, agrégat + contexte. La frontière n'est pas « lib crate vs bin crate » au sens de Cargo, mais « du code dont quelqu'un d'autre consomme les erreurs » vs « le bout de la chaîne ».'Is my code called by others, who might want to react to a precise failure?' Yes → library → thiserror, an enum the caller can match. No, I'm at the top of the stack and only reporting → application → anyhow, aggregate + context. The line isn't 'lib crate vs bin crate' in Cargo's sense, but 'code whose errors someone else consumes' vs 'the end of the chain'.

Le piège inverse aussi.The reverse trap too.

Symétriquement, écrire un enum thiserror exhaustif pour un petit binaire utilitaire, alors qu'on ne fera que printer l'erreur et sortir, c'est la cérémonie du Vol 3 qui revient : du type qui ne prévient aucun bug. Au sommet de la pile, anyhow + .context() suffit, et coûte trois lignes au lieu de trente. Le bon réflexe va dans les deux sens : ni anyhow en lib, ni enum cérémonie en app.Symmetrically, writing an exhaustive thiserror enum for a small utility binary, when you'll only print the error and exit, is Vol 3's ceremony returning: a type that prevents no bug. At the top of the stack, anyhow + .context() is enough, and costs three lines instead of thirty. The right reflex cuts both ways: neither anyhow in a lib, nor a ceremony enum in an app.

Le choix n'est pas thiserror ou anyhow : c'est préserver l'information chez qui pourrait réagir, l'agréger chez qui ne fait que rapporter.It isn't thiserror vs anyhow: it's preserving the information where someone might react, aggregating it where someone only reports.
№ 05Bilan & éditoWrap-up & editorial2 min

Une erreur n'est pas un message. C'est un type que tu conçois.An error isn't a message. It's a type you design.

Le n°1 a donné le contenant ; ce numéro l'a rempli. Une erreur utile garde la cause d'origine, se laisse trier par l'appelant, et s'affiche proprement — trois exigences que l'enum d'erreur satisfait, et que thiserror rend bon marché. Puis vient l'arbitrage qui décide de la forme : une bibliothèque préserve l'information pour des consommateurs qui voudront peut-être réagir ; une application l'agrège en un rapport lisible. Toujours le même geste — dire la vérité au compilateur, et la transmettre intacte à qui en aura besoin.Issue n°1 gave the container; this one filled it. A useful error keeps the original cause, lets the caller sort it, and displays cleanly — three demands the error enum satisfies, and that thiserror makes cheap. Then comes the trade-off that decides the shape: a library preserves the information for consumers who may want to react; an application aggregates it into a readable report. Always the same gesture — tell the compiler the truth, and pass it on intact to whoever will need it.

L'outilThe toolCe qu'il apporteWhat it bringsLe réflexeThe reflex
Enum d'erreurError enumUne variante par cause ; l'appelant peut matcher.One variant per cause; the caller can match.Quelqu'un voudra réagir → enum, pas un String.Someone will want to react → enum, not a String.
thiserrorDérive Display, Error et From — l'enum sans boilerplate.Derives Display, Error and From — the enum without boilerplate.Bibliothèque → thiserror + #[from] pour que ? convertisse.Library → thiserror + #[from] so ? converts.
anyhowUne erreur agrégée + une chaîne de contexte lisible.An aggregated error + a readable context chain.Application au sommet de la pile → anyhow + .context().Application at the top of the stack → anyhow + .context().
#[from] / sourceLa cause d'origine emboîtée, remontable par la chaîne.The original cause nested, walkable via the chain.N'efface pas l'erreur sous-jacente : emboîte-la.Don't erase the underlying error: nest it.
La forme d'un type d'erreur ne se déduit pas de l'échec, mais de ce que son lecteur en fera : réagir, ou rapporter.An error type's shape follows not from the failure, but from what its reader will do with it: react, or report.
Mot de l'éditeurFrom the editor

On a tout mis en Result. Il reste à savoir quand il est juste de tout arrêter.We put everything in a Result. What remains is knowing when it's right to stop everything.

Deux numéros durant, le mot d'ordre a été : ne panique pas, propage. Result pour l'échec attendu, un type qui dit ce qui a raté, et le ? pour le faire remonter. On pourrait en conclure qu'un programme bien écrit ne panique jamais. Ce serait une erreur — symétrique de celle du unwrap partout, mais erreur quand même.For two issues, the watchword has been: don't panic, propagate. Result for the expected failure, a type that says what went wrong, and ? to bubble it up. You might conclude a well-written program never panics. That would be a mistake — the mirror image of the unwrap-everywhere one, but a mistake all the same.

Vol 4 №3 : panic, unwrap & les invariants. Toutes les conditions ne méritent pas un Result. Quand un invariant qu'on s'était promis se rompt — un index qui ne devrait jamais déborder, un état déclaré impossible qui survient —, ce n'est pas une erreur attendue : c'est un bug, et continuer en faisant semblant serait pire que s'arrêter. panic!, unwrap, expect, unreachable!, assert! sont les outils de ce moment-là. On fermera le volume sur la frontière la plus subtile de toutes : celle entre l'échec qu'on gère et le bug qu'on refuse de masquer.Vol 4 №3: panic, unwrap & invariants. Not every condition deserves a Result. When an invariant you promised yourself breaks — an index that should never overflow, a state declared impossible that occurs — it isn't an expected error: it's a bug, and carrying on pretending would be worse than stopping. panic!, unwrap, expect, unreachable!, assert! are that moment's tools. We'll close the volume on the subtlest border of all: the one between the failure you handle and the bug you refuse to mask.