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.