runtimeanalyz.ing

← All capabilities

where_nil_comes_from

CLI · MCP free

Where nil comes from. The methods that returned nil and the arguments that received nil in recorded runs, logged as NilClass and tied to the errors that followed, including nil returns that a declared return type rules out.

Enrollment enrollment.rb:4
def unlock_next
  curriculum
    .next_after(current_book)
    .unlock!(student)
end
next_after ( current_book Book )
book Book
30 app runs · 2 spec calls
nil NilClass
1 app run · 0 spec calls
Curriculum recorded
# @return [Book]
def next_after(book)
  books[books.index(book) + 1]
end
✗ NoMethodError unlock! for nil · in Enrollment#unlock_next · the one app run where next_after returned nil
The comment says 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

  1. Each recorded call includes the class of what it returned. nil is an object like any other, so it is recorded as NilClass at the call that returned it.
  2. 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 the NoMethodError, which is how the nil return is tied to the error.
  3. 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 nil from params[: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 nil is 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.

how_it_got_here

CLI · MCP Pro

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

CLI · LSP · MCP free

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

CLI · LSP · MCP Pro

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 →