How To Guard Against Ruby Nil Errors

Chris
Ruby

Stop chasing down Ruby nil errors. Case in point:

1.9.3-p392 > user.first_name
NoMethodError: undefined method `first_name' for nil:NilClass

As a Ruby programmer, undefined method nil:NilClass can happen so regularly that it’s easy to fall into the habit of guarding against nils using &&’s or if’s to ensure we have a non-nil value. Consider the following:

class ApplicationController
  def current_user
    User.find_by_authenticated_session(session[:user_session_id])
  end
end

class User
  belongs_to :institution

  def institution_name
    institution && institution.name
  end
end

class Institution
  has_many :users
end

if current_user
  puts "First Name: #{current_user.first_name}"
  puts "Institution: #{current_user.institution_name}"
else
  puts "First Name: Guest"
  puts "Institution: Unknown"
end

1. Null Object Pattern

The above approach more or less adds type checks all around the codebase for type NilClass. A solution I’ve been using recently is to treat “No User” cases with the Null Object Pattern. Why represent a nil user with nil when we can wrap up the behavior in our system using a real object with specific behavior? Consider:

class ApplicationController
  def current_user
    user = User.find_by_authenticated_session(session[:user_session_id])
    user || NullUser.new
  end
end

class User
  belongs_to :institution
  alias_method :active_record_institution, :institution

  def institution
    self.active_record_institution || NullInstituion.new
  end

  def institution_name
    institution.name
  end
end

class NullUser
  def first_name
    "Guest"
  end
end

class NullInstituion
  def name
    "Unknown"
  end

  def users
    User.none
  end
end

This changes our previous example from 7 lines of code to 2!

puts "First Name: #{current_user.first_name}"
puts "Institution: #{current_user.institution_name}"

Treating a nil user and institution as real objects with behavior allows us to avoid the explicit nil type checks and treat these cases just like any other object in our system. Spread across the codebase, this adds up to much cleaner code and aids debugging nil errors that can often propagate up several levels of objects before being revealed.

2. Sentinel Objects

The Null Object Pattern can be overkill when all we want is to represent a nil object in our system that has no behavior other than being treated as the context of “None” for a given class. A sentinel object can be your best friend in this regard. Consider:

class Commission
  NoCommission = Class.new

  def self.for_user(user_id, since = 2.weeks.ago)
    commission = ExternalCRM.commission_for_employee(user_id, since)
    commission || NoCommission
  end
end

In the above example, if we happen to to receive no commission for the user given the time period, instead of leaking nil, we can pass back a NoCommission object. This can give context to the caller and prevent the dreaded nil stack trace. (i.e. NoMethodError: undefined method 'total' for #<NoCommission:0x007fa806c14818>. Now instead of tracing where our nil was produced, we know exactly where it originated simply by using a sentinel object.

3. Hash#fetch

Another easy way to prevent nil errors is to use Hash#fetch when pulling values out of hashes that you expect to be provided. For example:

class SMSNotifier
  def initialize(params = {})
    @user = params.fetch :user
    ...
  end

  def send_welcome_message
    ExternalSMS.deliver(@user.phone_number, "Welcome...")
  end
end

If params[:user] was nil, we would not find out until the first message was sent to to @user, at which point a nil error would be raised. Simply using Hash#fetch will raise KeyError: key not found: :user for us when pulling the user out of the params hash.

4. Use ActiveRecord “bang” methods

Simply using ActiveRecord bang methods create!, save!, etc that Rails provides will give you free debugging support when operations are expected to complete successfully by raising runtime errors.

Avoiding nil helps keep your Ruby stack traces relevant, gives better context to your callers, and lets you add behavior to real objects that would otherwise be handled by a series of ifs and nil type checks throughout your codebase.

Have a project we can help with?
Let's Talk

Get Started Today