Exception classes and custom errors

The StandardError tree

Every error is a class, and the class decides what a rescue catches. A bare rescue catches StandardError and its children: the errors a program can recover from. Exception is one level above that and includes the signals and the exit, which a program should not swallow.

ruby -e 'class E < StandardError; end; p E.ancestors.first(3)'

[E, StandardError, Exception]

A raise "x" builds a RuntimeError, the default class. Naming a class is how a caller tells errors apart:

ruby -e 'begin; raise ArgumentError, "bad input"; rescue => e; puts e.class; puts e.message; end'

ArgumentError
bad input

A project error class

A library or a project defines its own base class under StandardError, and every error it raises inherits from that. A caller can then rescue the project's errors without naming each one:

module Shop class Error < StandardError; end class OutOfStock < Error; end end begin raise Shop::OutOfStock, "no more" rescue Shop::Error => e puts e.message end

ruby -e 'module Shop; class Error < StandardError; end; class OutOfStock < Error; end; end; begin; raise Shop::OutOfStock, "no more"; rescue Shop::Error => e; puts e.message; end'

no more

rescue Shop::Error catches the stock error and any future error under the same parent. That is the cheap version of a stable interface: the leaves can change, the parent stays.

An error that carries data

An error class is a class, so it can hold the value that failed. The caller then has the id without parsing the message:

class NotFound < StandardError attr_reader :id def initialize(id) @id = id super("no record #{id}") end end begin raise NotFound, 42 rescue NotFound => e puts e.message p e.id end

ruby err.rb

no record 42
42

super sets the message, so e.message and the traceback stay normal. An error with a field is also the one place where a rescue can answer precisely: a web layer turns NotFound into a 404 and reads the id for the log.

The cause of an error

When a rescue raises a different error, Ruby keeps the original as cause. That is how a program tells its own story ("could not load the profile") without losing the real reason:

ruby -e 'begin; begin; raise "inner"; rescue => e; raise "outer"; end; rescue => e; p e.cause.message; end'

"inner"

cause is set automatically when the new raise happens inside a rescue. raise "outer", cause: nil clears it, and raise Other, "x", cause: original sets one explicitly when the new error is raised outside the rescue. A logged error usually prints the chain, not just the top message.

What to watch for

Never rescue Exception. It catches Interrupt and SystemExit, so Ctrl-C stops working and a test runner cannot exit. Rescue StandardError, a project parent, or the exact class.

A rescue that only logs and continues hides a failure. Either retry with a limit, raise a clearer error with the original as cause, or let it go. A bare rescue => e; end that does nothing is a promise the code cannot keep.

Next: ensure, at_exit and cleanup

Article author: Arthur Isaev