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>'
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.
You can preorder now to be among the first to gain access when it is released.