The Trouble With Nil
Do you know all the different ways a nil value might unexpectedly turn up in Ruby code?
In this episode, we’ll explore some of the myriad reasons you’re likely to see a nil appear. In the process you’ll learn about how nil’s meaning is always contextual, and why this should motivate us to use placeholder values other than nil whenever possible.
Video transcript & code
Consider the humble nil. For such an insignificant object, it sure does wear a lot of hats in Ruby. Let's look at a few of the ways we can come across a nil
in Ruby.
It's the default return value for a Hash when the key is not found:
h = {}
h['fnord'] # => nil
Of course, that doesn't necessarily mean that the key was not found, because the value might have been nil
.
h = {'fnord' => nil}
h['fnord'] # => nil
Empty methods return nil
by default.
def empty
# TODO
end
empty # => nil
If an if statement condition evaluates to false, and there is no else
, the result is nil
:
result = if (2 + 2) == 5
"uh-oh"
end
result # => nil
Likewise for a case statement with no matched when
clauses and no else
:
type = case :foo
when String then "string"
when Integer then "integer"
end
type # => nil
If local variable is only set in one branch of a conditional, it will default to nil
when that branch isn't triggered.
if (2 + 2) == 5
tip = "follow the white rabbit"
end
tip # => nil
Of course, unset instance variables always default to nil
. This makes typos especially problematic.
@i_can_has_spelling = true
puts @i_can_haz_speling # => nil
…and many Ruby methods return nil
to indicate failure:
[1, 2, 3].detect{|n| n == 5} # => nil
I could go on and on. There are countless ways to come across a nil
value in Ruby code.
Here's a method for fetching a password. When we call it, returns nil
.
require 'yaml'
SECRETS = File.exist?('secrets.yml') && YAML.load_file('secrets.yml')
def get_password_for_user(username=ENV['USER'])
secrets = SECRETS || @secrets
entry = secrets && secrets.detect{|entry| entry['user'] == username}
entry && entry['password']
end
get_password_for_user # => nil
Why did it return nil
? Let's make some hypotheses:
- Maybe the
secrets.yml
file exists, but contains no entries - Maybe the
USER
environment variable was not set. - Maybe
@secrets
wasnil
. - Maybe
@secrets
wasn'tnil
, but it just didn't contain an entry for the current user.
The sad truth is, there's absolutely no way of discovering through code inspection where that nil
came from. The nil
originated somewhere in this method, but like an uncooperative witness, it's not telling us anything.
And this is the fundamental problem with nil
: it can mean many different things, but the meaning is always contextual. It carries no semantics of its own. And so when we get that dreaded NoMethodError, we have to backtrack through the code, retracing its steps and trying to discover where meaningful data ended and a nil
was born.
I know you're probably expecting me to give you a solution to these woes, but unfortunately there is no single quick fix. I've been thinking about the problem of eliminating nil
values from my code for a long time, and recently I released the beta of a whole book on the subject. In the weeks to come I'll be drawing some of my RubyTapas topics from the patterns in that book, and we'll see several different techniques for stopping the nil
infestation from spreading through our code.
For now, I just want this episode to encourage you to remember that nil is rarely our friend, and that whenever possible it's better to replace it with a more meaningful value. Happy hacking!
Responses