def unlock_next
curriculum
.next_after(current_book)
.unlock!(student)
end # @return [Book]
def next_after(book)
books[books.index(book) + 1]
end Book, and the method returned a Book in 30 app runs and
in both spec calls. One app run returned NilClass, which no spec did, and an error
followed one line later.
The problem
Every so often the scheduler raises this:
NoMethodError: undefined method 'unlock!' for nil
app/models/scheduler.rb:13:in 'advance'
The stack trace shows where the nil was used and leaves out where it came from:
def advance(enrollment)
curriculum.next_after(enrollment.current_book).unlock!(enrollment.student)
end
def next_after(book)
ordered = books.to_a
index = ordered.index(book)
ordered[index + 1] if index
end
The name next_after suggests it returns a Book, and an RBS signature, if you had one, would say
the same. Neither is checked at runtime, and the one spec for the method advances from the first
book of three. The suite passes, and the declared type is wrong for one case.
The fix
Ask where the nil came from:
$ ra where_nil_comes_from Curriculum#next_after
Curriculum#next_after
· nil returns and nil arguments
Curriculum#next_after returned nil
1 of 31 app runs · 0 of 2 spec calls
1 of 1 unwound with NoMethodError in Scheduler#advance (app/models/scheduler.rb:13)
otherwise returned Book
next_after returned nil in the traffic run and in no spec. The untested case is a student on the
last book. Handle it where it happens, and write the missing spec:
def advance(enrollment)
next_book = curriculum.next_after(enrollment.current_book)
return enrollment.complete! if next_book.nil?
next_book.unlock!(enrollment.student)
end
it "completes the enrollment after the last book" do
enrollment = create(:enrollment, curriculum: curriculum, current_book: curriculum.books.last)
expect { Scheduler.new(curriculum).advance(enrollment) }.to change(enrollment, :finished?).to(true)
end
Run it without a method and you get the list for the whole app: every method that returned nil,
with its counts, most frequent first. Most of those nil returns are intended. FeeSchedule#charge
returns nil for a free plan and NullNotifier#deliver always returns nil, so the report lists them
all and leaves the judgement to you.
Where it shows up
- Over MCP, your agent asks about the method in the stack trace and gets the nil source, the exception that followed and the other return classes as structured blocks.
- On the command line, run
ra where_nil_comes_from Klass#method, or with no method for the whole app.
How it works
- Each recorded call includes the class of what it returned.
nilis an object like any other, so it is recorded asNilClassat the call that returned it. - When a traced method raises, the recording keeps the exception’s class as the call unwinds. The
nil return’s parent in the call tree is
Scheduler#advance, and its unwind row is theNoMethodError, which is how the nil return is tied to the error. - Every call tree is stamped with its entry point, so the report can say that a nil was seen in the traffic run and in no spec.
Run it on your app
The output on this page comes from our example Rails app, and it is regenerated whenever a tool
changes. These are the commands behind it. On your own app, run ra record first, then put your
own class, method or file in their place.
ra where_nil_comes_from 'Curriculum#next_after'
The method is app/models/curriculum.rb:19 and the caller that raises is app/models/scheduler.rb:13.
The traffic script advances every enrollment once and rescues the NoMethodError.
Reading the result
- A nil is seen when it is returned from, or passed to, a traced method. A
nilfromparams[:id]or a hash lookup that is used on the next line of the same method does not pass through a traced call, so it is not seen until it is returned or passed to one of your methods. - The report shows where a nil came from and does not judge whether it was intended. Sometimes
nilis the right answer and the caller should handle it, and sometimes the method shouldn’t return it. That decision is yours.
Every tool reads the same recording. What gets recorded and how far to trust an answer are on the questions page.
Related tools
how_it_got_here
How execution got here. The call chains that reached a method, in call order, taken from recorded runs. It is the stack trace you'd have got if the method had raised, for every route into it.
See more →what_it_receives
What a method receives. For each argument of a method, the classes it was given in recorded calls and the messages the method sent to it.
See more →observed_shape
The interface a call depends on. The messages your code sends to a value, read from recorded runs. They are the interface a call depends on, whether or not anyone declared it.
See more →