Posted by Martijn Versluis on 5-12-2018
Ruby and Rails include some techniques to “safely navigate” objects. However, using those techniques can lead to bad code.

First, let’s find out what safe navigation in Ruby looks like. Consider the following code that tries to get the avatar for the first group a user is member of:
1
2
3
def first_group_avatar_url
user.groups.first.avatar.file if user.groups.first && user.groups.first.avatar
end
Using ActiveSupport’s try method you could shorten this a little:
1
user.groups.try(:first).try(:avatar).try(:file)
Ruby 2.3 introduced the safe navigation operator (AKA “lonely operator”) to take up even less space:
1
users.groups&.first&.avatar&.file
I dislike all approaches, and here are five reasons why:
Reason #1: it is hard to read
No matter which variant you choose, the code looks cryptic and maybe even awkward. Besides that, when coming from a different programming language, it might even be a totally new concept to you.
Reason #2: it obscures intent
The code tells you something in the chain can be nil, but it does not tell you why. That makes it hard for another developer to change something in the code. You can add simple methods that explain it. See how this clearly explains the possible nils?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User
def member_of_a_group?
groups.any?
end
def first_group_avatar
user.groups.first.avatar.file if member_of_a_group? && groups.first.has_avatar?
end
end
class Group
def has_avatar?
!avatar.nil?
end
end
Reason #3: it attracts bad code
Just like all things that can be nil, the nil leaks into all code that uses it. Imagine some helper that wants to determine the URL for the group avatar. The avatar can be nil, so a new method call should deal with a possible nil. It is like a broken window attracting more broken windows.
1
2
3
4
avatar_url = user.first_group_avatar.try(:url)
# or:
avatar_url = user.first_group_avatar&.url
Reason #4: it (tends to) violate Demeter
Because using safe navigation operators make it easy to chain lots of method calls, it is also easy for violations to end up in the code. For example, you would determine the avatar URL for a group admin:
1
group.admin&.avatar&.url
However, according to the Law of Demeter, we should not have knowledge about group.admin, so in fact responsibility should be spread across method and objects and we should just call group.admin_avatar_url.
1
2
3
4
5
6
7
8
9
10
11
class Group
def admin_avatar_url
admin.avatar_url if admin
end
end
class Admin
def avatar_url
avatar.url if avatar
end
end
It you want, you can make the code more concise using ActiveSupport delegators. Personally, I find the vanilla version above easier to read though.
1
2
3
4
5
6
7
class Group
delegate :avatar_url, to: :admin, prefix: true, allow_nil: true
end
class Admin
delegate :url, to: :avatar, prefix: true, allow_nil: true
end
Reason #5: it makes bug hunting harder
Let’s say we implemented first_group_avatar_url using the “lonely operator”:
1
2
3
def first_group_avatar_url
users.groups&.first&.avatar&.url
end
Some part of the application uses our method, but you get a “undefined method for nil:NilClass” error. How will you find out which object was nil? Maybe the user was not a member of any group, maybe the group did not have an avatar, or maybe the avatar url was empty. Using safe navigation makes it harder to track down the cause of a NoMethodError.
When to use them
I do think there are occasional use cases. I tend to use &. or #dig when exploring a data structure inside IRB. The same applies to creating a prototype or quickly trying out an API integration.
Still navigating safe?
I hope I convinced you how to deal with nils in a proper way. Do you want to share your thoughts on this, or do you have questions? Please let me know.
At Kabisa we know a lot about writing great code. Leave us a message if you would like to get in touch.