How To Guard Against Ruby Nil Errors
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.