5 Ways to Deal with Nil Headaches in Ruby7 min read

Ruby is so much fun in many respects. Everything is an object and all our code are just objects passing messages to one another. We can do awesome, powerful things like metaprogramming with ease. We don’t have to worry about what a thing is, instead we’re liberated to focus on what it does.

There’s one glaring downside to this typeless, compiler-free language: nil. It’s everywhere. And it’s the cause of perhaps the most pervasive error in Ruby programming:

 

NoMethodError: undefined method 'something' for nil:NilClass

 

Some object somewhere ended up being nil. And, of course, nil doesn’t quack the way you want it to. Whether this presents itself as a dreadful 500 error that grinds your users to a halt, an exception that terminates some massive batch job, or just a bunch of noise is your already-crowded logs, it’s a huge pain.

But, it doesn’t have to be. If you’re mindful about your code and pay careful attention to situations where nil could happen, there are some proactive steps you can take to make your life (and your coworker’s lives) much easier.

 

#1) Tests

The first piece of advice I would give is to be consistent about writing thorough unit tests. Test what happens when your method sends a message to one of its objects when that object is nil. What happens? Assert what you want to happen instead, then go make the test pass.

If your method takes parameters, pass in nil. If it uses instances variables, make them nil. If your method has any internal dependencies, mock them as nils or stub them so they return a NoMethodError.

Now, assert some more useful behavior. Perhaps you still want to raise an exception, but now you can raise one with more context. Maybe you don’t want to raise an error at all, instead, you could have the method perform some graceful fallback and write to a logger. There are many ways to respond to a nil value, the right one typically depends on the situation. However, you can’t really know that your code does what you really want it to without test and, by extension, intentionally designing your code to be nil-resilient.

Testing this way has an added benefit. Rigorous unit testing tends to cause developers to keep their methods small. Small, simple methods tend to have fewer opportunities for nils to be lurking in the shadows.

 

#2) Safe Navigation Operator

So, you’re down in the weeds in some model. It wants to do something super simple, return the date of the last order placed by a given user.

 

class User 
  # ... a bunch of user-ish methods
  def last_ordered_at
    orders.last.placed_at
  end
end

 

One way to solve this would be to use Ruby’s relatively new safe navigation operator. Think of it as a Rails-free, less verbose version of the ‘.try’ syntax.

With it, we could refactor our method like so:

 

class User 
  # ... a bunch of user-ish methods
  def last_ordered_at 
    orders.last&.placed_at 
  end 
end

 

It’s hard to spot because the syntax is so subtle. But that simple change from ‘last.placed_at’ to ‘last&.placed_at’ will cause the whole chain to return nil in the event ‘orders.last’ turns out to be nil.

Is that better than an exception? Maybe, it depends. But be aware, your ‘last_ordered_at’ method can now return nil, itself potentially becoming a source of headaches to any code relying on it.

 

#3) Null Object Pattern

Dealing with null is so pervasive in programming that there’s an entire design pattern dedicated to mitigating it: the Null Object Pattern.

The idea is actually pretty clever. You create a “fake” version of a given object who stands in for nil and make it respond in a sensible way to certain messages.

Using our example above, we could build out this pattern like this:

 

class NullOrder 
  def placed_at 
    'No order history' 
  end 
end

class User
  def most_recent_order 
    orders.last || NullOrder.new 
  end

  def last_ordered_at 
    most_recent_order.placed_at 
  end 
end

 

Now, if the user hasn’t ordered anything before, our ‘placed_at’ message will be sent to the NullOrder object, which will then return our graceful fallback.

This approach is definitely more heavy-handed than a simply ‘&.’. On the other hand, it also allows us to concentrate our “none state” logic in one place. This pattern turns out to be a nice way of getting rid of (potentially many) if/else statements that could be scattered throughout our code.

Otherwise, we might have to do something like this in each and every one of our callers:

 

if user.last_ordered_at.present? 
  user.last_ordered_at 
else 
  'No order history' 
end

 

But with our null object, we can simply assert ‘user.last_ordered_at’.

This approach also has the benefit of being highly testable. We can mock NullOrder and assert very predictable output from last_ordered_at.

 

#4) Rescue NoMethodError

Sometimes, we don’t want our method to just keep humming along if nil is struck. Maybe we really do want to raise an exception, but that ‘NoMethodError’ isn’t super helpful in our stack trace.

To continue with our example, but assume that we’re really expected our user to have had an order. If she doesn’t, something is actually wrong in a deeper sense.

Let’s say we have an e-commerce system where the user can only create an account when checking out. As part of that process, their order is tied to their account (even if it’s not completed). So, by definition, a user is created with an order present.

In this case, we’ll actually want to raise an exception. We’ll also want it to be informative.

 

class UserMissingOrderError < StandardError 
end

class User
  def last_ordered_at
    orders.last.placed_at 
  rescue NoMethodError => e 
    raise UserMissingOrderError, "no order found for user with id #{id}"
  end 
end

 

The effect at runtime is that instead of a cryptic:

‘NoMethodError: undefined method ‘placed_at’ for nil:NilClass’

We’ll get:

‘UserMissingOrderError: no order found for user with id 1234’

Which, in this particular context, might save us a lot of debugging.

 

#5) Respect the Law (of Demeter)

The Law of Demeter, in a nutshell, tells us not to talk to strangers. That is, our class should send messages to objects it’s closely related to. It should stay away from distant relatives.

Say we want to know who the employees in our organization report to.

 

class Employee 
  def reports_to 
    department.supervisor.full_name 
  end 
end

 

We’re making several assumptions about the outside world here. We’re assuming that our employee will have a department, that the department will have a supervisor, and that the supervisor will have a full name.

According to the Law of Demeter, only the first assumption is appropriate. We can safely talk to the objects we “know”. In this case, our department. But we go further than that, asking our department to give us the contact info for their supervisor, so we can reach out to her and ask for a full name.

We’re both exposing ourselves to multiple potential nils and tightly coupling our code to the guts of far-flung objects we may have no control over. What if we want to refactor the relationship between departments and supervisors? Well, this unrelated code would break.

One way of cleaning this up would be to use message passing. Literally, have Department pass the message along so that Employee doesn’t have to worry about how Department deals with Supervisor.

 

class Department 
  def supervisor_name 
    supervisor.full_name 
  end 
end

class Employee 
  def reports_to 
    department.supervisor_name 
  end 
end

 

With this refactoring, the Department class now better encapsulates its affairs, shielding them from the prying eyes of the Employee class. If we need to refactor the relationship between Department and Supervisor in the future, we can do so without needing to change unrelated classes like Employee.

This might seem like the scenic route to Howtodealwithnils land. It’s worth it though: Law of Demeter violations are a chronic cause of nil struggles.

Wrapping Up

There are many ways to deal with ‘nil’. Most are situationally dependent. My main encouragement is to do more than toss is another if/else. Instead, examine the design of your code and ask yourself whether the problem isn’t with nil, but with the robustness of your code.

How do you deal with nil? Have you tried the patterns here, if so, what’s worked and what hasn’t?

As always, thanks for reading.

I’m a full-stack web developer from Seattle. I love building software with an eye for quality and writing about the process.

6 thoughts on “5 Ways to Deal with Nil Headaches in Ruby7 min read

  1. I like this list a lot. You might want to be careful with the language on #2. You state that the & “…will cause the whole chain to return nil…” But if you make a chained call like this: “orders&.last.title” you’d receive a NoMethodError.

    1. That’s a good point. I should have mentioned you would need to chain the “&.”‘s if you want the chain to return nil. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *