Level-up `rescue` with dynamic exception matchers

When you use a rescue clause in Ruby, you can specify what kinds of exceptions you want to rescue. But what if you want to rescue exceptions by severity? By message? By time of day? In this post we'll discuss how you can create dynamic exception matchers that use your logic to decide if an exception gets rescued.

When you use a rescue clause in Ruby, you can specify what kinds of exceptions you want to rescue. All you need to do is provide a list of exception classes like so:

begin
  raise RuntimeError
rescue RuntimeError, NoMethodError
  puts "rescued!"
end

But what if you don't know what the exception class will be at the time that you write the code? The most obvious answer is to rescue all exceptions, perform some kind of test and then re-raise the exceptions that don't pass. Something like this:

begin
  raise "FUBAR! The ship's going down!"
rescue => e
  raise unless e.message =~ /^FUBAR/
  ... do something ...
end

But that's so boring! Plus, it's not a very DRY approach. It would be a lot more interesting if we could somehow tell the rescue clause to only rescue exceptions matching our conditions. And since this is Ruby, we can do it!

How rescue matches exceptions

When an exception happens inside of a rescue block, the ruby interpreter checks the exception's class against the list of exception classes you provided. If there's a match, the exception gets rescued.

The matching looks something like this:

exception_classes_to_rescue.any? do |c|
  c === raised_exception.class
end

Just like every other operator in Ruby, === is simply a method. In this case it's a method of c. So what could we do if we defined our own === method?

In the example below I'm creating a class named Anything where  Anything === x  returns  true for any value of x. If I give this class as an argument to rescue, it causes all exceptions to be rescued. 

class Anything
  def self.===(exception)
    true
  end
end

begin
  raise EOFError
rescue Anything
  puts "It rescues ANYTHING!"
end

While there are much better ways to rescue all exceptions, this code is interesting because it shows us two things:

  1. You can give the rescue clause classes that don't inherit from Exception, as long as they implement ===

  2. If you control ===, you can control which exceptions are rescued.

Rescuing exceptions based on message

Knowing what we know now, it's simple to write code that only rescues exceptions if the exception's message matches a pattern.

class AllFoobarErrors
  def self.===(exception)
    # rescue all exceptions with messages starting with FOOBAR 
    exception.message =~ /^FOOBAR/
  end
end

begin
  raise EOFError, "FOOBAR: there was an eof!"
rescue AllFoobarErrors
  puts "rescued!"
end

Rescuing exceptions based on custom attributes

Since you have access to the exception object, your matcher can use any data contained inside that object.

Imagine for a moment that you have an exception that has a custom attribute called "severity." You'd like to swallow all "low severity" occurrences of the exception, but let pass any "high severity" occurrences. You might implement that like so:

class Infraction < StandardError
  attr_reader :severity
  def initialize(severity)
    @severity = severity
  end
end

class LowSeverityInfractions
  def self.===(exception)
    exception.is_a?(Infraction) && exception.severity == :low
  end
end

begin
  raise Infraction.new(:low)
rescue LowSeverityInfractions
  puts "rescued!"
end

Making it dynamic

All of this is pretty cool, but it does involve a lot of boilerplate code. It seems excessive to have to manually define separate classes for each matcher. Fortunately we can DRY this up quite a bit by using a little metaprogramming.

In the example below, we're defining a method that generates matcher classes for us. You provide the matching logic via a block, and the matching generator creates a new class that uses the block inside of its === method.

def exceptions_matching(&block)
  Class.new do
    def self.===(other)
      @block.call(other)
    end
  end.tap do |c|
    c.instance_variable_set(:@block, block)
  end
end

begin
  raise "FOOBAR: We're all doomed!"
rescue exceptions_matching { |e| e.message =~ /^FOOBAR/ }
  puts "rescued!"
end

A grain of salt

Like many cool tricks in Ruby, I can't quite decide if all of this is insanity or a great idea. Maybe it's a little of both. While I definitely wouldn't suggest that you reach for this technique as a first choice, I can see how it would be useful in situations like the one above where you want to rescue exceptions based on severity. In any case, it's another tool in your toolbelt!

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Starr Horne

    Starr Horne is a Rubyist and Chief JavaScripter at Honeybadger.io. When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

    More articles by Starr Horne
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial