In Progress
Unit 1, Lesson 21
In Progress

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
empty                           # => nil

If an if statement condition evaluates to false, and there is no else, the result is nil:

result = if (2 + 2) == 5
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"
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"

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']

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 was nil.
  • Maybe @secrets wasn't nil, 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!