Null Errors :: 0x14

Suppose you are helping a friend who is working on an exercise for a beginner's coding course.

The exercise involves creating a sample program to demonstrate some basic error handling techniques. But their implementation is not quite working as expected, and they've asked you to help you figure out what's going wrong.


The program is supposed to simulate syncing an inbox and displaying its new messages:

inbox = Inbox.new("gregory@skillstopractice.com")

inbox.sync_messages
inbox.each { |e| p e }

Most of the time, it will run successfully and display up to 10 sample messages with randomized subjects and bodies to the console:

{:subject=>"B", :body=>"Sweet"}
{:subject=>"A", :body=>"Sweet"}
{:subject=>"C", :body=>"Nice"}
{:subject=>"B", :body=>"Nice"}
{:subject=>"B", :body=>"Awesome"}
{:subject=>"B", :body=>"Nice"}
{:subject=>"B", :body=>"Awesome"}
{:subject=>"A", :body=>"Awesome"}
{:subject=>"A", :body=>"Sweet"}

But the sample program also needs to simulate a scenario where the mail server is down. When that happens, the program should print the following error log rather than displaying the messages:

Problem reaching the mail server, please try again.

This will have a 1 in 20 chance of occurring for each fetched message.


Your friend has their example program almost working, but has been scratching their head because in addition to the "Problem reaching the mail server" message being displayed, there's a stack trace showing the backtrace for a NoMethodError exception that they didn't expect:

Problem reaching the mail server, please try again
demo.rb:[...]:in `[...]': undefined method `...` for
nil:NilClass (NoMethodError)
	from demo.rb:...:in `<main>'
Note: The line numbers and method name have been intentionally redacted. Figuring out where they're coming from is part of the exercise, as you'll see below.

Simply seeing the nil:NilClass (NoMethodError) part of this trace makes you immediately aware that there's a null value somewhere in this code sample that there shouldn't be.

Their complete code listing is shown below. Skim through it quickly first, then read the questions that follow it, then read the code again as you try to answer them:

class Inbox
  def initialize(address)
    @address = address
  end

  def sync_messages
    @messages = MailServer.fetch_mail_for(@address)
  rescue
    STDERR.puts "Problem reaching the mail server, please try again"
  end

  def each
    @messages.each { |e| yield e }
  end
end

class MailServer
  def self.fetch_mail_for(address)
    rand(1..10).times.map { fetch_fake_message }
  end

  def self.fetch_fake_message
    raise if rand(1..20) == 1

    { :subject => ["A", "B", "C"].sample,
      :body => ["Nice", "Sweet", "Awesome"].sample }
  end
end

#--------------------------------------------------------------------

inbox = Inbox.new("gregory@skillstopractice.com")

inbox.sync_messages
inbox.each { |e| p e }

If you are able to answer the following questions correctly, you should be able to help your friend find and fix the bug in no time:

(1) What line causes the NoMethodError exception to be raised?

# [ANSWER 1] #

 # in Inbox#each definition

 @messages.each { |e| yield e }

(2) Which method call actually triggered the NoMethodError to be raised?
(in other words, what name would sappear in the undefined method '...' part of the stack trace?)

# [ANSWER 2] #

The @messages.each { ... } call raises a NoMethodError because @messages is nil.

demo.rb:[..]:in `[..]': undefined method `each' for

(3) What incorrect assumption was made that caused this error to be raised?

# [ANSWER 3] #

 The Inbox#each method assumes that @messages will always be present
 (even if it might be empty)

 However, when MailServer.fetch_mail_for raises an exception, this
 prevents @messages from ever being set in Inbox#sync_messages.

 If @messages were set to an empty array in Inbox#initialize, this
 problem would not occur.

The field notes for this exercise are not included in the free sample, but will be provided in the paid version of this guidebook.

You can preorder now to be among the first to gain access when it is released.