Expecting Exceptions in Ruby


Sometimes in Ruby code, having to use the begin ... rescue .. end construction to capture and deal with exceptions can feel overly burdensome. I recently found myself in such a situation and wrote a small function to make my code read a bit more elegantly.


I was recently writing some code that I needed to be particularly resilient to certain errors, and I found myself with a method that looked something like this:

1
2
3
4
5
6
7
def my_method
  # ...
rescue SomeError
  # ...
rescue SomeOtherError
  # ...
end

Now, to be fair, this code worked precisely as expected and is the kind of code you might see in various online examples for handling exceptions. However, something about this style just didn’t sit right on that day. As I probed my mild annoyance, I realized that this code reminds me of the if ... elsif ... else ... end structure, and I have developed a sense that such code in a method is a smell to be refactored with guard clauses. The code above smelled like conditional code, and when I smell conditional code that leads to distinct return paths, I want to refactor to guard clauses.

Unfortunately, Ruby doesn’t have a mechanism to rescue from a particular error inline; the best you can do is do_something rescue false, which will rescue all errors.1 What I’d love is some semantics like:

1
2
3
4
5
6
def my_method
  return false if { do_something } rescue SomeError
  return false if { do_something_else } rescue SomeOtherError
  
  # ...
end

Since Ruby doesn’t give us such semantics, I went ahead and wrote a simple function to get me as close as possible:

1
2
3
4
5
6
def throws?(exception) # &block
  yield
  return false
rescue Exception => e
  return e.is_a? exception
end

It always returns a Boolean and works like this:

1
2
3
4
5
6
7
8
throws?(StandardError) { raise }
# => true
throws?(NameError) { raise NameError }
# => true
throws?(NoMethodError) { raise NameError }
# => false
throws?(StandardError) { 'foo' }
# => false

So, our example method could be written like so:

1
2
3
4
5
6
def my_method
  return false if throws?(SomeError) { do_something }
  return false if throws?(SomeOtherError) { do_something_else }
  
  # ...
end

To be clear, this method is not a replacement for the begin ... rescue .. end construction in every situtation; however, in certain situations it does allow for guard clauses that rescue specific errors.

  1. For why this is a bad idea, I’d recommend this blogpost by Thoughtbot.