Advanced Exception Objects

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.

Example of an exception's message attribute

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