Exception objects hold all of the information about "what happened" during an exception.
We've seen how you can retrieve the exception object when rescuing:
begin
...
rescue StandardError => e
puts "Got the exception object: #{ e }"
end
Now let's dig in and see just what information these objects contain.
» What Are Exception Objects?
Exception objects are instances of exception classes. It's that simple.
In the example below, we create a new exception object and raise it. If we catch it, we can see that it's the same object that we raised.
my_exception = RuntimeError.new
begin
raise my_exception
rescue => e
puts(e == my_exception) # prints "true"
end
Not all classes can be used as exceptions. Only those that inherit from Exception
can.
Remember our chart of the exception class hierarchy? Exception
is right at the top:
Exception
NoMemoryError
ScriptError
LoadError
NotImplementedError
SyntaxError
SignalException
Interrupt
StandardError
ArgumentError
IOError
EOFError
..etc
» The Class
The first — and probably most important — piece of information about an exception object is it's class name. Classes like ZeroDivisionError
and ActiveRecord::RecordNotFound
tell you exactly what went wrong.
Because your code is unique, it can be useful to define your own exception classes to describe an exceptional condition. To do this, simply create a class that inherits from another kind of Exception.
The most common approach is to inherit from StandardError
. As long as your exception is a kind of error, it's a good idea to follow this practice.
class MyError < StandardError
end
raise MyError
» Namespacing
It's a good idea to namespace your new exception classes by placing them in a module. For example, we could put our MyError
class in a module named MyLibrary
:
class MyLibrary::MyError < StandardError
end
I haven't used namespacing in the code examples in this book to save space and to avoid confusing beginners who might think that the module syntax is a requirement.
» Inheritance
You've seen how rescuing StandardError
not only rescues exception of that class, but of all its child classes as well.
You can take advantage of this behavior to create your own easily-rescuable exception groups.
class NetworkError < StandardError
end
class TimeoutError < NetworkError
end
class UnreachableError < NetworkError
end
begin
network_stuff
rescue NetworkError => e
# this also rescues TimeoutError and UnreachableError
# because they inherit from NetworkError
end
» The Message
When your program crashes due to an exception, that exception's message is printed out right next to the class name.
To read an exception's message, just use the message
method:
e.message
Most exception classes allow you to set the message via the intialize
method:
raise RuntimeError.new("This is the message")
# This essentially does the same thing:
raise RuntimeError, "This is the message"
The exception's message is a read-only attribute. It can't be changed without resorting to trickery.
» The Backtrace
You know what a backtrace is, right? When your program crashes it's the thing that tells you what line of code caused the trouble.
The backtrace is stored in the exception object. The raise
method generates it via Kernel#caller
and stores it in the exception via Exception#set_backtrace
.
The trace itself is just an array of strings. We can examine it like so:
begin
raise "FOO"
rescue => e
e.backtrace[0..3].each do |line|
puts line
end
end
This prints out the first three lines of the backtrace. If I run the code in IRB it looks like this:
/irb/workspace.rb:87:in `eval'
/irb/workspace.rb:87:in `evaluate'
/irb/context.rb:380:in `evaluate'
You can also set the backtrace, which is useful if you ever need to "convert" one exception into another. Or perhaps you're building a template engine and want to set a backtrace which points to a line in the template instead of a line of Ruby code:
e2 = ArgumentError.new
e2.set_backtrace(e1.backtrace)
» Your Own Data
Adding "custom" attributes to your exception classes is as easy as adding them to any other class.
In the following example, we create a new kind of exception called MyError
. It has an extra attribute called thing
.
class MyError < StandardError
attr_reader :thing
def initialize(msg="My default message", thing="apple")
@thing = thing
super(msg)
end
end
begin
raise MyError.new("my message", "my thing")
rescue => e
puts e.thing # "my thing"
end
» Causes (Nested Exceptions)
Beginning in Ruby 2.1 when an exception is rescued and another is raised, the original is available via the cause
method.
def fail_and_reraise
raise NoMethodError
rescue
raise RuntimeError
end
begin
fail_and_reraise
rescue => e
puts "#{ e } caused by #{ e.cause }"
end
# Prints "RuntimeError caused by NoMethodError"
Above, we raised a NoMethodError
then rescued it and raised a RuntimeError
. When we rescue the RuntimeError
we can call cause
to get the NoMethodError
.
The causes mechanism only works when you raise the second exception from inside the rescue block. The following code won't work:
# Neither of these exceptions will have causes
begin
raise NoMethodError
rescue
end
# Since we're outside of the rescue block, there's no cause
raise RuntimeError
» Nested Exception Objects
The cause
method returns an exception object. That means that you can access any metadata that was part of the original exception. You even get the original backtrace.
In order to demonstrate this, I'll create a new kind of exception called EatingError
that contains a custom attribute named food
.
class EatingError < StandardError
attr_reader :food
def initialize(food)
@food = food
end
end
Now we'll rescue our EatingError
and raise RuntimeError
in its place:
def fail_and_reraise
raise EatingError.new("soup")
rescue
raise RuntimeError
end
When we rescue the RuntimeError
we can access the original EatingError
via e.cause
. From there, we can fetch the value of the food
attribute ("soup") and the first line of the backtrace:
begin
fail_and_reraise
rescue => e
puts "#{ e } caused by #{ e.cause } while eating #{ e.cause.food }"
puts e.cause.backtrace.first
end
# Prints:
# RuntimeError caused by EatingError while eating soup
# eating.rb:9:in `fail_and_reraise'
» Multiple Levels of Nesting
An exception can have an arbitrary number of causes. The example below shows three exceptions being raised. We catch the third one and use e.cause.cause
to retrieve the first.
begin
begin
begin
raise "First"
rescue
raise "Second"
end
rescue
raise "Third"
end
rescue => e
puts e.cause.cause
# First
end