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