Vol. 7 · № 02 · Un atelier RubyA Ruby workshop

Concevoir sa hiérarchie, ou StandardError jamais Exception Designing your hierarchy, or StandardError never Exception

Les exceptions forment un arbre où une frontière commande tout : StandardError (le rattrapable, et tes erreurs) sous Exception (qui contient aussi les signaux). Hérite de StandardError, rattrape StandardError, jamais Exception — sinon on avale le Ctrl-C. Et bannis le rescue fourre-tout qui rend nil. Exceptions form a tree where one boundary commands everything: StandardError (the rescuable, and your errors) under Exception (which also holds the signals). Inherit from StandardError, rescue StandardError, never Exception — or you swallow Ctrl-C. And ban the catch-all rescue that returns nil.

AudienceAudience
Dev qui veut des erreurs qui parlent, et un rescue qui ne ment pas Dev who wants errors that speak, and a rescue that doesn't lie
Format
Self-paced
ChapitresChapters
5
Date
Mai 2026 May 2026
≈ 18 min ●●●○ StandardErrorHiérarchieException
SommaireContents ·
№ 01CadrageFraming3 min

Les exceptions forment un arbre — et une frontière y compte.Exceptions form a tree — and one boundary matters in it.

Le № 1 rattrapait par type ; mais ce tri ne vaut que par la qualité de l'arbre qu'on lui donne. Toutes les exceptions descendent d'Exception. Juste en dessous, une frontière décisive sépare deux mondes : StandardError, où vivent les erreurs ordinaires d'un programme (et où doivent aller les tiennes), et tout ce qui est sous Exception mais hors de StandardError — les signaux système : Interrupt (le Ctrl-C de l'utilisateur), SystemExit, NoMemoryError. Ce ne sont pas des bugs à gérer, ce sont des ordres de l'extérieur. C'est pour ça que rescue, sans précision, s'arrête à StandardError : il laisse passer les signaux.№ 1 caught by type; but that sorting is only as good as the tree you give it. Every exception descends from Exception. Just below, a decisive boundary splits two worlds: StandardError, home to a program's ordinary errors (and where yours belong), and everything under Exception but outside StandardError — the system signals: Interrupt (the user's Ctrl-C), SystemExit, NoMemoryError. These aren't bugs to handle, they're orders from outside. That's why rescue, unqualified, stops at StandardError: it lets the signals through.

L'arbre, et la ligne StandardErrorThe tree, and the StandardError line
# l'arbre des exceptions — et la frontière qui compte
Exception                       # la racine de TOUT
├─ NoMemoryError                #   ┐
├─ SignalException → Interrupt  #   │ hors de StandardError :
├─ SystemExit                   #   │ les signaux système, à NE PAS rattraper
├─ ScriptError                  #   ┘
└─ StandardError                # ← le défaut de rescue, et la maison de TES erreurs
   ├─ RuntimeError              #   raise "msg" en crée une
   ├─ ArgumentError, KeyError   #   les erreurs « normales » d'un programme
   └─ NoMethodError, TypeError

raise "boom"               # => RuntimeError, donc < StandardError
RuntimeError.ancestors     # => [RuntimeError, StandardError, Exception, Object, ...]
Deux mondes sous ExceptionTwo worlds under Exception
Hors StandardErrorOutside StandardError
Interrupt · SystemExit
NoMemoryError
des ordres — laisse passerorders — let them through
StandardErrorStandardError
RuntimeError · KeyError
tes erreurs maison
des bugs — gère-les icibugs — handle them here

La règle tient en une ligne : hérite de StandardError, rattrape StandardError — jamais Exception. La frontière n'est pas cosmétique : d'un côté ce que ton programme peut gérer, de l'autre ce qu'il doit laisser le tuer.The rule fits one line: inherit from StandardError, rescue StandardError — never Exception. The boundary isn't cosmetic: on one side what your program can handle, on the other what it must let kill it.

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

« J'hérite de StandardError, je rattrape StandardError, jamais Exception. » Tes classes d'erreur descendent de StandardError pour qu'un rescue ordinaire les voie ; et tu ne rattrapes jamais Exception, pour ne pas avaler les signaux que l'utilisateur ou le système t'envoient."I inherit from StandardError, I rescue StandardError, never Exception." Your error classes descend from StandardError so an ordinary rescue sees them; and you never rescue Exception, so you don't swallow the signals the user or system sends you.

L'arbre est une chaîne d'ancêtres.The tree is an ancestor chain.

Rien de neuf depuis le Vol 5 : rescue KeyError attrape KeyError et ses descendants parce que rescue teste l'appartenance avec === (donc is_a?) le long de la chaîne. Rattraper une classe, c'est rattraper tout son sous-arbre. C'est précisément ce qui rend une hiérarchie maison si utile : une classe de base bien placée attrape toute une famille d'erreurs d'un seul rescue.Nothing new since Vol 5: rescue KeyError catches KeyError and its descendants because rescue tests membership with === (hence is_a?) along the chain. Catching a class catches its whole subtree. That's exactly what makes a custom hierarchy so useful: one well-placed base class catches a whole family of errors with a single rescue.

№ 02La surpriseThe surprise4 min

rescue Exception avale le Ctrl-C de l'utilisateur.rescue Exception swallows the user's Ctrl-C.

C'est le piège le plus grave du volume, et il vient d'une confusion d'un seul mot. rescue Exception semble « plus sûr » que rescue StandardError — il attrape davantage. C'est justement le problème : il attrape ce qu'on ne doit jamais attraper. Le Ctrl-C lève une Interrupt, qui descend d'Exception mais pas de StandardError ; un exit lève SystemExit ; une panne mémoire, NoMemoryError. Rattraper Exception, c'est intercepter ces ordres venus de l'extérieur — et rendre un programme impossible à arrêter, ou masquer une mort imminente. Le défaut de Ruby est juste : rescue s'arrête à StandardError pour laisser les signaux faire leur travail.It's the volume's gravest trap, and it comes from confusing a single word. rescue Exception looks 'safer' than rescue StandardError — it catches more. That's exactly the problem: it catches what you must never catch. Ctrl-C raises an Interrupt, which descends from Exception but not StandardError; exit raises SystemExit; an out-of-memory, NoMemoryError. Rescuing Exception intercepts these orders from outside — making a program impossible to stop, or masking an imminent death. Ruby's default is right: rescue stops at StandardError to let the signals do their job.

Attraper Exception, c'est attraper l'inarrêtableCatching Exception catches the unstoppable
# Le piège mortel : rescue Exception attrape AUSSI les signaux
loop do
  begin
    poll_queue
  rescue Exception => e     # Exception = la racine → attrape TOUT
    logger.warn(e)
    next
  end
end

# l'utilisateur appuie sur Ctrl-C → lève Interrupt (< Exception, hors StandardError)
#   … mais le rescue l'attrape et fait « next » : impossible de quitter.
# un SystemExit (exit) ? avalé aussi. Un NoMemoryError ? on tourne en boucle.

« Plus large » n'est pas « plus sûr ». En attrapant la racine Exception, ce rescue intercepte le Interrupt du Ctrl-C et le SystemExit d'un exit — l'utilisateur martèle le clavier et le programme refuse de mourir. On a confondu « gérer mes erreurs » avec « gérer tout événement », et capturé des ordres qui ne m'étaient pas adressés."Broader" isn't "safer". By catching the root Exception, this rescue intercepts Ctrl-C's Interrupt and an exit's SystemExit — the user hammers the keyboard and the program refuses to die. You've confused "handle my errors" with "handle any event", capturing orders not addressed to you.

rescue Exception n'est pas plus prudent — c'est refuser à l'utilisateur le droit d'arrêter ton programme.rescue Exception isn't more careful — it's denying the user the right to stop your program.
№ 03La mécaniqueThe mechanics5 min

Une classe de base par domaine — et des données sur l'objet, pas un message.One base class per domain — and data on the object, not a message.

Concevoir une hiérarchie d'erreurs suit deux principes simples. Un : une classe de base par domaine (Billing::Error < StandardError), dont héritent toutes les erreurs de ce domaine — rattraper la base attrape la famille entière, rattraper une feuille cible un cas. Deux : une exception est un objet, alors fais-lui porter des données structurées, pas seulement une chaîne. needed et available comme attributs, c'est un appelant qui réagit sans jamais parser un message. La hiérarchie n'est pas de la cérémonie : c'est ce qui donne au rescue par type du № 1 de quoi trier finement, et au code appelant de quoi décider.Designing an error hierarchy follows two simple principles. One: a base class per domain (Billing::Error < StandardError), inherited by every error in that domain — rescuing the base catches the whole family, rescuing a leaf targets one case. Two: an exception is an object, so make it carry structured data, not just a string. needed and available as attributes means a caller reacts without ever parsing a message. The hierarchy isn't ceremony: it's what gives № 1's rescue-by-type something to sort finely, and the calling code something to decide on.

Une racine de domaine, des feuilles qui portent des donnéesA domain root, leaves that carry data
module Billing
  # la RACINE de ta famille — une seule classe à rattraper pour tout prendre
  class Error < StandardError; end

  class CardDeclined < Error; end
  class GatewayDown  < Error; end

  class InsufficientFunds < Error
    def initialize(needed:, available:)
      @needed, @available = needed, available
      super("besoin de #{needed}, dispo #{available}")   # message lisible…
    end
    attr_reader :needed, :available                       # … ET données structurées
  end
end
Choisir la granularité du rescueChoosing the rescue granularity
begin
  charge(card)
rescue Billing::InsufficientFunds => e   # le cas précis : on a les données
  top_up(e.needed - e.available)         # pas de parsing de message !
rescue Billing::Error => e               # toute la famille d'un coup
  notify_support(e)
end
# rattraper la base prend tout le sous-arbre ; rattraper une feuille
# cible un cas — la hiérarchie te laisse choisir la granularité
L'erreur dit ce qui a échoué.The error says what failed.

Un bon arbre d'erreurs parle. Billing::InsufficientFunds nomme le problème par sa classe ; ses attributs (needed, available) le décrivent en données exploitables. L'appelant rattrape ce qu'il sait traiter, à la granularité qu'il veut — un cas précis, ou toute la famille via la base. Compare à un RuntimeError générique avec un message à disséquer : la classe maison transforme « quelque chose a planté » en « voici quoi, et de quoi réagir ». Hériter de StandardError (jamais d'Exception) garde tout ça dans le monde du rattrapable.A good error tree speaks. Billing::InsufficientFunds names the problem by its class; its attributes (needed, available) describe it as usable data. The caller rescues what it knows how to handle, at the granularity it wants — one precise case, or the whole family via the base. Compare a generic RuntimeError with a message to dissect: the custom class turns "something broke" into "here's what, and what to act on". Inheriting from StandardError (never Exception) keeps all of it in the rescuable world.

Un repère de dosage : crée une classe d'erreur quand un appelant réagira différemment selon le cas, ou aura besoin des données qu'elle porte. Si personne ne va distinguer ni exploiter l'erreur, un RuntimeError avec un message clair suffit — la hiérarchie se construit là où le tri sert, pas par principe.A sizing cue: create an error class when a caller will react differently per case, or needs the data it carries. If no one will distinguish or use the error, a RuntimeError with a clear message is enough — the hierarchy is built where the sorting serves, not on principle.

№ 04Le piègeThe trap4 min

Le rescue => e; nil fourre-tout : tous les bugs deviennent du silence.The catch-all rescue => e; nil: every bug becomes silence.

Le contre-exemple obligatoire du numéro, et peut-être l'anti-pattern le plus coûteux de Ruby. On entoure un calcul d'un rescue => e; nil « au cas où » — et on vient de transformer toute erreur en valeur nil muette. Une faute de frappe qui lève NoMethodError, une connexion qui tombe, un bug de logique : tout est avalé, rien n'est logué, la méthode rend nil comme si de rien n'était. Le bug ne disparaît pas, il se déplace : il refait surface trois couches plus loin, sous une forme méconnaissable, loin de sa cause — exactement le mal du Vol 3. La parade : ne rattrape que la famille que tu sais gérer, réagis, et re-lève le reste.The issue's mandatory counter-example, and perhaps Ruby's costliest anti-pattern. You wrap a computation in rescue => e; nil 'just in case' — and you've just turned every error into a mute nil. A typo raising NoMethodError, a dropped connection, a logic bug: all swallowed, none logged, the method returns nil as if nothing happened. The bug doesn't vanish, it moves: it resurfaces three layers away, in an unrecognizable form, far from its cause — exactly Vol 3's affliction. The remedy: rescue only the family you know how to handle, react, and re-raise the rest.

Avaler tout, rendre nil : le bug devient muetSwallow all, return nil: the bug goes mute
def total(orders)
  orders.sum(&:amont)        # faute de frappe : amont au lieu d'amount
rescue => e                  # le fourre-tout : rescue StandardError…
  nil                        # … et on AVALE tout, on rend nil
end

total(orders)   # => nil
# le NoMethodError n'a jamais levé, jamais été logué : il est devenu un nil
# muet qui casse trois couches plus loin — l'erreur loin de sa cause (Vol 3)
Rattraper son domaine, laisser remonter le resteRescue your domain, let the rest climb
def total(orders)
  orders.sum(&:amount)
rescue Billing::Error => e   # on ne rattrape QUE ce qu'on sait gérer
  report(e)                  # on réagit…
  raise                      # … et on re-lève : la décision reste à l'appelant
end

# un vrai bug (NoMethodError, faute de frappe) n'est PAS un Billing::Error :
#   il n'est pas rattrapé, il remonte, visible, à sa cause. Le silence n'existe pas.
Le contre-exemple obligatoire.The mandatory counter-example.

Un rescue ne doit attraper que ce qu'il sait traiter. Le fourre-tout rescue => e; nil attrape aussi ce qu'il ne comprend pas — un bug — et le fait disparaître. C'est ici que la hiérarchie du chapitre 3 paie : en rattrapant Billing::Error et non StandardError, un NoMethodError dû à une faute de frappe n'entre pas dans le filet, donc remonte à sa cause, visible. Et même dans le cas géré, raise nu re-lève après avoir réagi : on informe sans jamais étouffer. La règle : rattrape étroit, journalise, re-lève — le silence n'est jamais une stratégie d'erreur.A rescue must catch only what it knows how to handle. The catch-all rescue => e; nil also catches what it doesn't understand — a bug — and makes it vanish. This is where chapter 3's hierarchy pays off: by rescuing Billing::Error and not StandardError, a NoMethodError from a typo doesn't enter the net, so it climbs to its cause, visible. And even in the handled case, bare raise re-raises after reacting: you inform without ever smothering. The rule: rescue narrow, log, re-raise — silence is never an error strategy.

Un rescue qui rend nil ne supprime pas le bug — il le déguise en valeur, loin de sa cause, pour qu'il frappe plus tard et ailleurs.A rescue returning nil doesn't remove the bug — it disguises it as a value, far from its cause, so it strikes later and elsewhere.
№ 05Bilan & éditoWrap-up & editorial2 min

Hérite de StandardError, rattrape étroit, ne tais jamais.Inherit from StandardError, rescue narrow, never silence.

Les exceptions forment un arbre, et une frontière y commande tout : StandardError, où vivent les erreurs gérables et les tiennes ; au-dessus, les signaux qu'on laisse passer. D'où la règle : hérite de StandardError, rattrape StandardError, jamais Exception — sinon on avale le Ctrl-C. Une hiérarchie maison, une base par domaine, fait parler les erreurs : la classe nomme, les attributs décrivent, le rescue choisit sa granularité. Et l'interdit absolu : le rescue fourre-tout qui rend nil et change tout bug en silence.Exceptions form a tree, and one boundary commands everything: StandardError, home to handleable errors and yours; above it, the signals you let through. Hence the rule: inherit from StandardError, rescue StandardError, never Exception — or you swallow Ctrl-C. A custom hierarchy, one base per domain, makes errors speak: the class names, the attributes describe, the rescue picks its granularity. And the absolute taboo: the catch-all rescue that returns nil and turns every bug into silence.

La questionThe questionLa règleThe ruleLe réflexeThe reflex
De quoi hériter ?What to inherit from?StandardError, toujours — jamais Exception.StandardError, always — never Exception.Tes erreurs vivent dans le monde rattrapable.Your errors live in the rescuable world.
Quoi rattraper ?What to rescue?Une base de domaine (App::Error), pas Exception.A domain base (App::Error), not Exception.rescue Exception avale le Ctrl-C : jamais.rescue Exception swallows Ctrl-C: never.
Que porte l'erreur ?What does the error carry?Des données structurées, pas qu'un message.Structured data, not just a message.L'appelant réagit sans parser une chaîne.The caller reacts without parsing a string.
Et ce que je ne gère pas ?And what I don't handle?Laisse-le remonter — ou re-lève après réaction.Let it climb — or re-raise after reacting.rescue => e; nil est l'anti-pattern à bannir.rescue => e; nil is the anti-pattern to ban.
Un bon arbre d'erreurs dit ce qui a échoué ; un mauvais rescue fait taire la réponse. Conçois le premier, n'écris jamais le second.A good error tree says what failed; a bad rescue silences the answer. Design the first, never write the second.
Mot de l'éditeurFrom the editor

Une hiérarchie d'erreurs est une interface, pas un détail.An error hierarchy is an interface, not a detail.

On traite souvent les classes d'erreur comme un à-côté — un RuntimeError et un message, vite fait. Ce numéro défend l'inverse : l'arbre des exceptions est une part de l'interface d'un module, au même titre que ses méthodes publiques. Il dit à l'appelant ce qui peut échouer, et sous quelle forme il pourra réagir. La frontière StandardError / Exception n'est pas une trivia : elle encode la distinction entre ce que ton programme gère et ce qu'il doit laisser le tuer. Et le rescue fourre-tout n'est pas une maladresse mineure, c'est un effacement de l'information la plus précieuse qu'un système produise — la nouvelle qu'il vient de se tromper.Error classes are often treated as an afterthought — a RuntimeError and a message, quickly. This issue argues the opposite: the exception tree is part of a module's interface, as much as its public methods. It tells the caller what can fail, and in what form they can react. The StandardError / Exception boundary isn't trivia: it encodes the distinction between what your program handles and what it must let kill it. And the catch-all rescue isn't a minor slip, it's the erasure of the most valuable information a system produces — the news that it just got something wrong.

Prochain numéro : exceptions vs valeurs de retour. On a appris à lever et à structurer ; le № 3 demande quand NE PAS lever. Une exception coûte une stack trace et détourne le flux ; pour un cas attendu — clé absente, fin de fichier, validation qui échoue — une valeur de retour (un nil, un objet Result) dit souvent mieux ce que l'exception criait. On clôt le volume sur le discernement : l'exception est pour l'exceptionnel, pas pour le contrôle de flux.Next issue: exceptions vs return values. We learned to raise and to structure; № 3 asks when NOT to raise. An exception costs a stack trace and diverts the flow; for an expected case — missing key, end of file, failing validation — a return value (a nil, a Result object) often says better what the exception shouted. We close the volume on discernment: the exception is for the exceptional, not for control flow.