Episode Script
Let’s say we need to pull out some user info from an Omniauth-style hash.
auth = {
'uid' => 12345,
'info' => {
'email' => '[email protected]'
'first_name' => 'avdi',
'last_name' => 'grimm'
},
'credentials' => {
'token' => "TOKEN123"
}
}
User = Struct.new(:email_address, :first_name, :last_name, :token)
u = User.new
u.email_address = auth['info']['email']
u.first_name = auth['info']['first_name']
u.last_name = auth['info']['last_name']
u.token = auth['credentials']['token']
Later on, we use this user info with the assumption that it was all collected successfully.
greeting = "Good morning, #{u.first_name.capitalize}"
Unfortunately, there’s no guarantee in the Omniauth hash schema that any of these fields will be supplied by a given provider. If we add a provider that omits some or all of these fields, we may get a rude surprise.
greeting = "Good morning, #{u.first_name.capitalize}"
greeting # =>
# ~> -:4:in `<main>': undefined method `capitalize' for nil:NilClass (NoMethodError)
The worst thing about this error is that there’s no direct connection between the error and the missing auth info. The occurrence of the error may be widely separated in code and in time from the point where the user object was originally populated.
It would be better to catch the fact that some fields are missing from the auth info at the point where the fields are first extracted. The presence of these fields is a part of our assumption, and it’s always a good idea to verify our assumptions about data from external sources.
We might do this by adding statement modifiers to each assignment:
u.email_address = auth['info']['email'] or raise ArgumentError
# ...
But this is tedious. There’s a much more concise and idiomatic way to do this, which is to use the #fetch
method instead of the subscript ([]) operator.
u.email_address = auth['info'].fetch('email'])
u.first_name = auth['info'].fetch('first_name')
u.last_name = auth['info'].fetch('last_name')
u.token = auth.fetch('credentials').fetch('token')
Note that we use fetch for the nested “credentials” hash as well, because it too is not guaranteed to be present.
The #fetch
method on Hash
behaves similarly to the subscript operator except that it is more strict. If the specified key is missing, it raises a KeyError
.
auth = {
'uid' => 12345,
'info' => {
}
}
# ...
u.email_address = auth['info'].fetch('email')
# ~> -:11:in `fetch': key not found: "email" (KeyError)
# ~> from -:11:in `<main>'
This error is an improvement in a few ways: first, it occurs at the point where the fields are first extracted. So there’s no question about the origin of the missing value. And the error message clearly indicates that there was a missing key named “email”, unlike the NoMethodError
we saw before.
Whenever I need a value from a hash, I try to think about whether I have any expectations about that value. If I expect it to always be found, that means I’ll probably be writing other code that depends on that value being present. In that case, I usually opt for a call to #fetch
instead of using the subscript operator. It’s a few extra characters that buys me peace of mind.
In a future episode I’ll talk about some of the other features of the #fetch
method, but this is enough for now. Happy hacking!
In one of the main applications I deal with at work I feel like there is a constant drift seeing more and more ‘.try’ methods thrown on things when nil values are occasionally being passed in. I have been upping my game to combat this, but had not yet encountered this method. There’s a fair few places where we are rendering views or writing to the database off of hashes, I will definitely be including this method in the future! (And I’m already thinking about a personal project that relies on an external API for its data that would definitely benefit from this too). Thanks for the episode!
I’m glad you found it helpful!
This is amazing. This tip alone was worth the monthly sub! At work, we just got done finding a very rare NoMethod Error resulting in a memory leak on a very large and old codebase and if the original author(s) of the code had used fetch from the beginning, it would have saved hundreds of hours of work.
useful! Thanks.
You’re welcome!