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 KeyError
s, it also happily threw away NoMethodError
s 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!