Project

General

Profile

Feature #10982

Clarify location of NoMethod error

Added by schneems (Richard Schneeman) about 2 years ago. Updated almost 2 years ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:<unknown>]

Description

In Ruby, the error NoMethodError on happens frequently, especially when it occurs on nil. This error can be confusing to beginners since, many of them think there is a problem with the method instead of the receiver. This error can be confusing to advanced developers when there are multiple method calls in the on the same line. In this example it is unclear if foo or bar returned nil:

foo.call && bar.call
NoMethodError: undefined method `call' for nil:NilClass

I would like to make this a better error message and to indicate where the exception occurs.

@foo.call && @bar.call
                 ^----
NoMethodError: The method `call` is undefined on receiver nil:NilClass

Now it is more clear that the @bar is currently nil and that is the source of the error. I believe exposing this information will help developers of all abilities.

History

#1 Updated by parkr (Parker M) about 2 years ago

This new syntax is a pretty sizable divergence from traditional Ruby error reporting. At the moment, errors generally (always?) have an accompanying stack trace:

~$ ruby errors.rb
errors.rb:2:in `<main>': undefined method `call' for nil:NilClass (NoMethodError)

It indicates the line, but excludes the column. It's uniform for all errors, so users seeing these stack traces know in what file, what line, and what method (the binding?) the error occurred. If the proposed format were implemented, would it only be for NoMethodError's, or would it apply to ArgumentError's or TypeError's, and so on?

Would the column number suffice? So your example would yield:

irb:1:16:in `<main>': undefined method `call' for nil:NilClass (NoMethodError)

You'd know that line 1 at column 16 caused the error.

#2 Updated by shevegen (Robert A. Heiler) about 2 years ago

I think ruby warnings do not start with capitalized character such as in "The" :)

By the way your two examples are not equivalent right? Because in the first example
you use either a local variable, or a method called foo, and in the second you
use an instance variable called @foo

#3 Updated by yuki24 (Yuki Nishijima) almost 2 years ago

It wasn't so hard to implement this in pure Ruby, the last example doesn't work, though.

# -*- coding: utf-8 -*-
class NoMethodError
  REPL_LABELS = {
    "irb_binding" => -> { Readline::HISTORY.to_a.last },
    "__pry__"     => -> { ::Pry.history.to_a.last }
  }

  def to_s
    msg = super
    msg << "\n\n".freeze
    msg << "  #{code}\n"
    msg << "  #{marker}\n"
  rescue
    super
  end

  def code
    # Memoize since the #backtrace_locations method creates a lot of objects.
    @code ||= begin
      loc = backtrace_locations.first

      if REPL_LABELS.include?(loc.label)
        REPL_LABELS[loc.label].call
      elsif File.exist?(loc.absolute_path)
        File.open(loc.absolute_path) do |file|
          file.detect { file.lineno == loc.lineno }
        end
      end
    end.to_s.strip
  end

  def marker
    code.strip.gsub(/(.*)(#{name}).*/) do
      (" ".freeze * $1.size.pred) + '^'.freeze + ("".freeze * $2.size)
    end if !code.empty?
  end
end

# works
e = 1.foo rescue $!
puts e
# => undefined method `foo' for 1:Fixnum
#
#  e = 1.foo rescue $!
#       ^‾‾‾

# works
e = 1.send(:bar) rescue $!
puts e
# => undefined method `bar' for 1:Fixnum
#
#  e = 1.send(:bar) rescue $!
#             ^‾‾‾

# works
@foo = ->{ true }
e = (@foo.call && @bar.call) rescue $!
puts e
# => undefined method `call' for nil:NilClass
#
#  e = (@foo.call && @bar.call) rescue $!
#                        ^‾‾‾‾

# doesn't work
e = (@something.call && @else.call) rescue $!
puts e
# => undefined method `call' for nil:NilClass
#
#  e = (@something.call && @else.call) rescue $!
#                               ^‾‾‾‾

Also available in: Atom PDF