Safe navigation in Ruby


Navigation through embedded objects can suprise us with an error if one of the fields, which are objects as well, is null. We are in the same situation if we have a hash object and want to get a value by key if key does not exist. The burning situation crops up if we chain up methods next to each other.

In this post I write some approaches, how to keep our code still clean, readable and safe if we liked to chain up calls next one another on an object or on a key-value pair container.

Demonstration of the core problem comes below:

Address = Struct.new(:city, :street, :number)
Person = Struct.new(:name, :age, :address)

people = []
people[0] = Person.new('jancsi', 12, Address.new('City', 'somewhere over the rainbow', '42'))
people[1] = Person.new('jancsi', 12)

people.each do |person|
	p person.address.street
end
# => 'somewhere over the rainbow'
# => `<main>': undefined method `street' for nil:NilClass (NoMethodError)

And in case of hash, the same problem looks like that:

animals = Hash.new
animals['mammals'] = {'even-toed' => 'giraffe'}                                 
animals['reptiles'] = {'snakes' => { 'cobra' => ['king cobra', 'tree cobra']} }

['mammals', 'reptiles', 'birds'].each do |key|
  p animals[key]['snakes']
end
# => nil
# => {"cobra"=>["king cobra", "tree cobra"]}
# => Traceback (most recent call last):
# ...
# => navigation.rb:29:in `block in <main>': undefined method `[]' for nil:NilClass (NoMethodError)

Solution in Ruby

Ruby introduced the safe navigation operator & in version 2.3, with which we can handle null objects.

people.each do |person|
	p person&.address&.street
end
# => "somewhere over the rainbow"
# => nil

What if we liked to get a value based on its key and chaining the indices next one another. If one of the key is missing, indexing would be applied against a null object. Furtonately, the || operator comes handy.

p (animals['mammals'] || {})['snakes']  # => nil
p (animals['reptiles'] || {})['snakes']  # => {"cobra"=>["king cobra", "tree cobra"]}
p (animals['birds'] || {})['snakes']  # => nil

If the value belongs to the set of falsy values like false we get the same result, though false can be a valid index.

Let us see what we can do to enhance fetching values by keys from a hash.

Hash types brings dig method:

p animals.dig('mammals', 'snakes')  # => nil
p animals.dig('reptiles', 'snakes')  # => {"cobra"=>["king cobra", "tree cobra"]}
p animals.dig('birds', 'snakes')  # => nil

Gems

Older versions of Ruby (below 2.3) do not support safe navigation, so gems had been popped up to fill the voidness. Rails was also one of the firsts that provided a solution. It introduced try function to check whether an indexing returns null or not.

Rails introduced try to achieve that functionality.
The above code looks like as follows:

people.each do |person|
	p person.try(:address).try(:street)
end
# => 'somewhere over the rainbow'
# => nil

So we have got some insight how to handle embedded objects and avoid raising NoMethodError exceptions.

Code can be found: https://github.com/torokmark/safe-navigation-in-languages