Better null-check with Safe Navigation operator in Ruby
If you are familiar with the Optional Chain operator (?.
) of JavaScript, then you cannot miss the Safe Navigation Operator (&.
) of Ruby.
Now let's dig into it!
What is a Safe navigation operator?
Have you ever seen this error before?
NoMethodError (undefined method 'foo' for nil:NilClass)
This is a common error that occurs whenever you try to call a property or method on an nil
object in Ruby. Then you might have ended up writing lots of conditional statements to check for nil
before accessing the property or method.
To overcome this problem, Ruby provides a shorthand operator called the Safe navigation operator (&.
) or lonely operator that helps you avoid the NoMethodError
, which was introduced in Ruby 2.3.0. The long code will become shorter and more readable:
# Without `&.`
user && user.job && user.job.company && user.job.company.address
# With `&.`
user&.job&.company&.address
A detailed example:
user = User.find_by(id:)
if user&.email
puts "User's email: #{user.email}"
else
puts "User has no email address"
end
In the code above, we're trying to access the email
property of user
object. However, if the user
is nil
, calling directly user.email
will result in NoMethodError
. By using the safe navigation operator (user&.email
), it means:
- if
user
is notnil
, then call itsemail
and print it out; - otherwise, return
nil
then print out"User has no email address"
.
Caution!
- The safe navigation operator only works for
nil
objects. Otherwise, it will raise theNoMethodError
for non-nil
objects. This is also a feature that gives a right error for an unimplemented method.
'foo'&.email # => undefined method `email' for "foo":String
12345&.email # => undefined method `email' for 12345:Integer
false&.email # => undefined method `email' for false:FalseClass
- Do not overuse it! The safe navigation operator can mask errors that should be caught and handled properly, which makes it harder to find bugs.
# Assume user not found
user = nil
# Instead of checking if user is nil, the flow still do below things,
# which cost resources and lack of error handling for important cases
send_notification(user&.email)
verify_company(user&.job&.company&.id)
get_direction(user&.address)
Why is it better than try
?
If you're on Rails, or using ActiveSupport
, you can use present?
or try
to check a nil
object:
# With `present?`
user.present? && user.job.present? && user.job.company
# => true / false
# With `try`
user.try(:job).try(:company)
# => nil / user.job.company
However, try
will return nil
for non-nil
objects even if the method or property doesn't exist. Instead of try
, try!
is recommended instead, it works as the same as the safe navigation operator.
'foo'.try(:email) # => nil
'foo'.try!(:email) # => undefined method `email' for "foo":String
Benchmark
You can try the following code to compute the performance between try
and &.
:
require "benchmark"
require "active_support/all"
Benchmark.bm do |x|
count = 1_000_000
x.report "active_support try" do
count.times { nil.try(:length) }
end
x.report "safe navigation &." do
count.times { nil&.length }
end
end
According to the results, the safe navigation operator &.
is about four times faster than try
, as it's a built-in of Ruby. It does not depend on ActiveSupport
and other dependencies of Rails:
use | time |
---|---|
active_support try |
0.116621 |
safe navigation &. |
0.034128 |
Summary
- The safe navigation operator (
&.
) gives a shorter syntax, faster performance, making the code more readable and easier to maintain if used appropriately. - The safe navigation operator should only be used when
nil
is an acceptable result. Make sure that you're not using it as a substitute for proper error handling. - Using
try
ortry!
are slightly longer than the&.
, which can make it less readable. Despite that, they should still be used in the older versions that don't have the&.
available.