Episode #022: Inline Rescue

Upgrade to download episode video.

Episode Script

So we have this method which, internally, uses a Hash#fetch to get a value. If the key it's looking for doesn't exist, it raises a KeyError.

def user_name(params)
  params.fetch(:username)
end

user_name(username: 'Frank')    # => "Frank"
user_name({})                   # => 
# ~> -:2:in `fetch': key not found: :username (KeyError)
# ~>    from -:2:in `user_name'
# ~>    from -:6:in `<main>'

We have some other code that uses this method.

def greet(params)
  name = user_name(params)
  puts "Hello, #{name}"
end
greet({})
# ~> -:2:in `fetch': key not found: :username (KeyError)
# ~>    from -:2:in `user_name'
# ~>    from -:5:in `greet'
# ~>    from -:9:in `<main>'

It turns out that for the purposes of this code, we don't really care if the username is missing; we'd like to just replace it with a placeholder and keep going. We make this happen by using rescue as a statement modifier to rescue the KeyError and return the string “Anonymous”.

def greet(params)
  name = user_name(params) rescue "Anonymous"
  puts "Hello, #{name}"
end
greet({})
# >> Hello, Anonymous

When rescue is used at the end of a line it is will rescue any exception descended from StandardError and return the value of the expression following it. This works nicely for our method and we move on to other things.

Time passes, and one day the implementation of the #user_name method is changed in order to track changes in the incoming hash format, which now has separate first name and last name fields.

def user_name(params)
  "#{params.fetch(:fname)} #{params.fethc(:lname)}"
end

Our “missing name” substitution continues to work just fine.

greet({})
# >> Hello, Anonymous

But then we discover a problem: the method always greets an anonymous user, even when first name and last name are provided.

greet(fname: "Jane", lname: "Doe")
# >> Hello, Anonymous

We bang our heads against the wall trying to figure this out for twenty minutes, until finally we take a closer look at the updated #user_name method. Turns out, one of the calls to #fetch had a transposed letter!

def user_name(params)
  "#{params.fetch(:fname)} #{params.fethc(:lname)}"
end

Why didn't we see this typo immediately? Because the inline rescue in the #greet method not only ate KeyErrors, it also happily threw away NoMethodErrors as well. We can see this when we remove the rescue.

def greet(params)
  name = user_name(params)
  puts "Hello, #{name}"
end
greet(fname: "Jane", lname: "Doe")
# ~> -:2:in `user_name': undefined method `fethc' for {:fname=>"Jane", :lname=>"Doe"}:Hash (NoMethodError)
# ~>    from -:5:in `greet'
# ~>    from -:9:in `<main>'

Rescuing an overly-general exception class is a common source of errors in software. Since the statement-modifier form of rescue can't take an exception class to match, it is overly broad by definition. That's why despite it's convenience, I consider rescue as a statement modifier to be a code smell wherever I see it. I've seen it lead to problems time and time again.

I have one exception to this rule: occasionally, we may want to convert an exception to a return value, for instance if we want to examine the exception before deciding to re-raise it, log it, or handle it some other way. Inline rescue gives us an elegant way to accomplish this.

value_or_error = {}.fetch(:some_key) rescue $!
value_or_error # => #<KeyError: key not found: :some_key>

This code takes advantage of the fact that Ruby always stores a reference to the currently-raised exception (if any) in the $! variable. So the exception is rescued, and then the value returned from the rescue is the exception itself.

If we want a somewhat more readable version, we can require the English library and use the alias $ERROR_INFO for $!.

require 'English'
value_or_error = {}.fetch(:some_key) rescue $ERROR_INFO
value_or_error # => #<KeyError: key not found: :some_key>

Note however that this will only rescue StandardError, not exceptions derived directly from the base Exception class.

To sum up: rescue as a statement modifier, while temptingly convenient, is usually a bad idea. However, it's an excellent way to convert exceptions into return values.

That's it for today. Happy hacking!