Method Lookup and super

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them. Every method call in Ruby asks one question: starting from the receiver, which method object has this name? The answer is decided by a single, linear list of modules and classes, the ancestor chain, searched from the front. Everything else on this page follows from that list: what include, prepend and extend really do, why super can work inside a module that has no superclass, how a Method object can tell you where a definition lives, what happens when nothing is found and method_missing gets its turn, and why wrapping a method with prepend is safer than the old alias chain.

The page builds on Classes and Objects, Modules and Mixins and Singleton Classes and Class-Level Behavior, and repeats the signature rules of Methods and Parameters only where super needs them. Visibility checks happen after a method is found and are in Visibility and Encapsulation; refined methods are consulted before this chain and are in Refinements and Monkey Patching; generating methods and calling them by name is in Dynamic Dispatch and Method Definition, the hooks that fire when the chain changes are in Hooks, eval and DSLs, and the reflection API that inspects it is in Reflection, Introspection and Tracing. The exceptions raised by a failed lookup are catalogued in Exceptions and Error Handling. For comparison, Python resolves methods with the C3 linearisation described in Python classes, and Java resolves them through single inheritance plus default interface methods (see Java inheritance).

Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings, NewCops: enable, Style/Documentation and Style/OneClassPerFile disabled because each example is a self-contained file with several small classes). The output shown is the real output. Files that use the shelf library are run with ruby -Ilib, so require "shelf" finds it (see the shelf gem source); the others are standalone.

The lookup path

The ancestor chain

When Ruby evaluates receiver.name, it takes the singleton class of the receiver, which is where the search starts, and walks its ancestors in order until a method called name is found. Module#ancestors returns that list, and for ordinary (non-singleton) classes the list is built from four ingredients, in this order:

  1. The modules that the class prepends, the most recently prepended first.

  2. The class itself.

  3. The modules that the class includes, the most recently included first.

  4. The superclass, and then the same three steps for it, up to BasicObject.

Before all of that comes the singleton class of the receiver itself, with the modules added by extend. A method defined with def object.name or inside class << object is found first, because it is the first entry of the chain. Each entry of the chain is visited once, and a module that appears twice (included by a class and by its superclass, or included twice) is kept only at its first position in the chain.

flowchart TD C["receiver.name(args)"] --> S["1. singleton class of the receiver, including modules added by extend"] S -->|not found| P["2. modules prepended to the class, last prepended first"] P -->|not found| K["3. the class itself"] K -->|not found| I["4. modules included in the class, last included first"] I -->|not found| U["5. the superclass: repeat steps 2 to 4 up to BasicObject"] U -->|not found| M["6. method_missing on the receiver, which starts a second lookup"] M -->|BasicObject#method_missing| E["NoMethodError or NameError"] S -->|found| R["run the method"] P -->|found| R K -->|found| R I -->|found| R U -->|found| R M -->|user-defined method_missing| R R -->|super| N["continue the same chain after the module or class that holds this method"]

The next file builds a class with all five sources of methods and asks the objects where each answer came from. Audited is prepended, Titled is included, Item is the superclass, and a singleton method is defined on a single instance. The final lines show that the same view applies to a class from the shelf gem.

# frozen_string_literal: true

require "shelf"

# One object, five places a method can come from.
module Audited
  def to_s = "[audited] #{super}"
end

module Titled
  def to_s = "titled(#{super})"
  def label = "label from Titled"
end

class Item
  def to_s = "an item"
  def label = "label from Item"
  def weight = 1
end

class Shelved < Item
  include Titled
  prepend Audited

  def to_s = "shelved>#{super}"
end

copy = Shelved.new
# a singleton method on one object is found first
def copy.weight = 2

puts copy
puts copy.label
puts copy.weight
p Shelved.ancestors.first(5)
p copy.singleton_class.ancestors.drop(1).first(4)
p copy.method(:to_s).owner
p copy.method(:label).owner
p copy.method(:weight).owner.singleton_class?
p Shelf::Catalog.instance_method(:each).owner
p Shelf::Catalog.ancestors

Output:

[audited] shelved>titled(an item)
label from Titled
2
[Audited, Shelved, Titled, Item, Object]
[Audited, Shelved, Titled, Item]
Audited
Titled
true
Shelf::Catalog
[Shelf::Catalog, Enumerable, Object, Kernel, BasicObject]

Reading the output: to_s is found in Audited before the class Shelved is even looked at, and Audited#to_s calls super, which continues from the entry after Audited. label is not defined by Shelved, so the search passes the class and finds the included Titled before reaching Item, which also defines it. weight is found in the singleton class of that one object, and singleton_class? confirms it. Shelf::Catalog#each has Shelf::Catalog as its owner, and Enumerable sits between the class and Object because Catalog includes it. Kernel is the only module between Object and BasicObject.

The chain is a flat list, so there is no "diamond problem" to resolve: a method is found at the first position that defines it, and super always means "the next position". Which position a module takes is decided only by whether it was included, prepended or extended, and by the order of those calls.

Mechanism Where the module goes Typical use

include M

Directly after the class, before the superclass; the last include is searched first.

Adding behaviour that the class may override (Comparable, Enumerable).

prepend M

Directly before the class; the last prepend is searched first.

Wrapping or decorating methods the class defines itself; instrumentation.

extend M on an object

Directly after the singleton class of that object (it is singleton_class.include M).

Adding methods to one object or to a class as class methods.

class << obj or def obj.name

The singleton class itself, the first entry.

Per-object methods, class methods.

Class.new(Parent) or class Child < Parent

The new class is placed before Parent; the whole chain of Parent follows.

Inheritance.

include, prepend and extend in detail

include inserts a module after the class; with several arguments the first one named is searched first, so include Loggable, Cacheable puts Loggable ahead of Cacheable (the arguments are processed right to left, and the last processed is searched first). Including a module that is already in the chain does nothing. prepend inserts a module before the class, and has the same rule about repeated calls. The hooks included, prepended and extended, and the methods they are called by (append_features, prepend_features, extend_object), are covered in Hooks, eval and DSLs.

# frozen_string_literal: true

module Loggable; end
module Cacheable; end
module Late; end

Base = Class.new

class Plain < Base
end
Plain.include(Loggable, Cacheable) # Loggable is searched first
p Plain.ancestors.first(4)

class Again < Base
  include Cacheable
  include Loggable
  include Cacheable # already in the chain: no effect
end
p Again.ancestors.first(4)

# Since Ruby 3.0 a module changed after inclusion is seen by its includers.
class Host < Base
  include Loggable
end
Loggable.include(Cacheable)
Loggable.prepend(Late)
p Host.ancestors.first(5)

# prepend puts the module in front of the class itself
module Wrapper; end

class Front < Base
  prepend Wrapper
  include Cacheable
end
p Front.ancestors.first(4)
p Front.include?(Wrapper), Front.ancestors.index(Wrapper) < Front.ancestors.index(Front)

Output:

[Plain, Loggable, Cacheable, Base]
[Again, Loggable, Cacheable, Base]
[Host, Late, Loggable, Cacheable, Base]
[Wrapper, Front, Cacheable, Base]
true
true

Four details are worth remembering. First, Plain.include(Loggable, Cacheable) puts Loggable first, and so do two separate statements in the opposite order (include Cacheable and then include Loggable): the module included last is searched first. Second, repeating include Cacheable in Again changes nothing, because the module is already in the chain. Third, since Ruby 3.0 a change made to a module after a class included it is visible to that class: Loggable.include(Cacheable) and Loggable.prepend(Late) both reached Host, which had included Loggable earlier. Before 3.0 only classes that included the module afterwards saw the change, which is why older tutorials say "include modules into a module before including it anywhere". Fourth, a class that prepends a module also gets the modules that module includes, in front of itself, which is why the example uses a fresh Wrapper module for Front.

prepend has one more property: it is the only built-in way to put code in front of a method that the same class defines, because the prepended module is searched before the class. That is what makes the wrapping technique in the section below possible.

Class methods follow the same rules

A class method is a singleton method of the class object, so it is looked up in the singleton class of the class. The singleton class of a subclass inherits from the singleton class of its superclass, which is why class methods are inherited, and extend on a class adds a module to that singleton chain. super inside a class method therefore continues along the singleton chain.

# frozen_string_literal: true

# Class methods are singleton methods, so they follow the singleton-class chain.
class Repository
  def self.build(*) = new(*)
  def self.table = "repositories"

  def self.inherited(subclass)
    super
    subclass.instance_variable_set(:@registered, true)
  end

  def initialize(name = "default") = @name = name
  attr_reader :name
end

class BookRepository < Repository
  def self.table = "books:#{super}"
end

module Prefixed
  def table = "app_#{super}"
end

BookRepository.extend(Prefixed)

p BookRepository.table
p BookRepository.build("main").name
p BookRepository.singleton_class.ancestors.first(4)
p BookRepository.method(:build).owner
p BookRepository.method(:table).owner
p BookRepository.method(:table).super_method.owner
p BookRepository.singleton_class.superclass
p BookRepository.instance_variable_get(:@registered)

# Instances do not see class methods: they come from the class, not its singleton class
begin
  BookRepository.new.table
rescue NoMethodError => e
  puts e.message
end

Output:

"books:app_repositories"
"main"
[#<Class:BookRepository>, Prefixed, #<Class:Repository>, #<Class:Object>]
#<Class:Repository>
#<Class:BookRepository>
Prefixed
#<Class:Repository>
true
undefined method 'table' for an instance of BookRepository

BookRepository.table runs BookRepository.table first (its own singleton method), whose super reaches Prefixed#table (the extended module) and whose super reaches Repository.table. BookRepository.singleton_class.superclass is #<Class:Repository>, which proves that the singleton classes mirror the class hierarchy, and BookRepository.new.table fails because instances start at BookRepository, not at its singleton class. The inherited hook in the example calls super first, the rule for every hook that other modules may also define. The structure of singleton classes is explained in Singleton Classes and Class-Level Behavior.

Where refinements and the cache fit

Active refinements are consulted before the chain described above: for a class C Ruby checks the refinements of C (most recently activated first), then the prepended modules of C, C, and its included modules, and only then repeats the search for the superclass. A method in a subclass beats a refinement of its superclass, which surprises people who refine Numeric and then call Integer#/. See Refinements and Monkey Patching and the refinements syntax documentation.

The result of a lookup is cached by the virtual machine, and the cache is invalidated when a method is defined, removed, aliased, or when the chain changes. That is an implementation detail and not a behaviour you can rely on, but it explains why defining methods in a hot loop, or including modules at run time in a tight request path, is expensive. The page Performance, Benchmarking and Profiling returns to this.

Finding out where a method comes from

Reading the chain by eye does not scale, so Ruby exposes it through Method objects. Object#method(:name) returns the Method that would run (it raises NameError when there is none), and Module#instance_method(:name) returns an UnboundMethod for a class or module. Both answer owner (the module or class whose body holds the definition), name and original_name (differ for aliases), arity, parameters, source_location (a file and line, nil for methods written in C) and super_method, the method that super would reach. Following super_method until it returns nil lists every definition of that name in the chain.

# frozen_string_literal: true

require "shelf"

# Method objects answer "which method will run, and where is it defined?"
module Shouting
  def to_s = super.upcase
end

class Reader < Shelf::Catalog
  include Shouting

  def to_s = "reader>#{super}"
end

reader = Reader.new
method = reader.method(:to_s)
chain = []
while method
  chain << [method.owner, method.source_location&.last]
  method = method.super_method
end
chain.each { |owner, line| puts "#{owner} #{line.inspect}" }

m = reader.method(:each)
p m.owner, m.receiver.equal?(reader), m.name, m.arity
p m.unbind.class

unbound = Shelf::Catalog.instance_method(:each)
p unbound.bind(reader).call.class
p unbound.bind_call(reader).class

begin
  unbound.bind(Object.new)
rescue TypeError => e
  puts e.message
end

p Reader.instance_method(:to_s).owner
p Reader.public_instance_method(:to_s).super_method&.owner
p Reader.method_defined?(:to_s, false), Reader.method_defined?(:each, false), Reader.method_defined?(:each)
p Reader.instance_methods(false)

Output:

Reader 13
Shouting 7
Kernel nil
Shelf::Catalog
true
:each
0
UnboundMethod
Enumerator
Enumerator
bind argument must be an instance of Shelf::Catalog
Reader
Shouting
true
false
true
[:to_s]

The first loop walks to_s through Reader (line 13), Shouting (line 7) and the C implementation in Kernel (no source location). The second block shows the Method of each: its owner is Shelf::Catalog even though it was called on a Reader, and unbind returns an UnboundMethod that must be bound to an instance of the owner (or a subclass, or, for a module method, any object): binding to an unrelated object raises TypeError. bind_call binds and calls in one step without allocating an intermediate Method, the usual way to call a method of a specific module or class regardless of overrides. The final lines show that method_defined? and instance_methods can be limited to the class itself with false.

The other tools worth knowing are:

Tool What it answers

Module#ancestors

The full chain of a class or module.

Object#singleton_class.ancestors

The chain including the singleton class and extended modules.

Module#include?(mod), Module#<(other), Module#<=(other)

Whether a module is in the chain, and how two modules relate (nil if unrelated).

Object#method, Module#instance_method, Module#public_instance_method

The method that would run; public_instance_method refuses private and protected methods.

Method#owner, #receiver, #super_method, #source_location

Where it is defined, whose it is, the next definition, and its file and line.

Module#method_defined?(name, inherit = true), #instance_methods(inherit = true)

Whether the public or protected method exists, and the list of them; false limits the answer to the module itself.

Module#undefined_instance_methods

The names blocked in this class by undef_method.

Object#respond_to?(name, include_all = false)

Whether a call would succeed, honouring respond_to_missing?.

Class#superclass, Class#subclasses, Module#nesting

The parent, direct children, and lexical nesting (the last is about constants, not methods).

How super finds the next method

super calls the method of the same name that comes after the current method’s owner in the ancestor chain of the receiver. Two things follow. The "current" position is the module or class whose body contains the def, not the class of self. And the "chain" is the one of self, not the one of the module being defined, so the next method depends on who includes the module.

The forms of super, with their argument rules, are in the section on the forms of super; in short, bare super passes the current values of the method’s parameters (including keywords and the block), super() passes no arguments, and super(a, k: 1) passes exactly what is written. In all three the block of the current call goes along unless you write &nil.

Form Arguments passed Block passed

super

The current values of every parameter of the method, including optional, rest, keyword and keyword-rest parameters.

The current block.

super()

None.

The current block.

super(a, b) or super(a, k: 1)

Exactly what is written.

The current block.

super(a, &blk) or super(&nil)

Exactly what is written.

The block blk, or no block.

super(...)

The arguments captured by a ... parameter.

Included in ....

The next file shows a cooperative chain. Trace is prepended, Strict is included and Job is the superclass, and each layer calls super once, so every layer runs and the innermost one yields to the block. The same Greeting module is then mixed into two unrelated classes, and its super reaches a different method in each. defined?(super) lets a module work both with and without a successor, and &nil withholds the block.

# frozen_string_literal: true

# A cooperative chain: every layer calls super and passes the block on.
module Trace
  def run
    ["Trace", *super]
  end
end

module Strict
  def run
    ["Strict", *super]
  end
end

class Job
  def run
    [yield]
  end
end

class Email < Job
  include Strict
  prepend Trace

  def run
    ["Email", *super]
  end
end

result = Email.new.run { "block" }
p result
p Email.ancestors.first(4)

# The same module is spliced into different chains, so its super differs.
module Greeting
  def greet = "hello, #{super}"
end

class Visitor
  def greet = "visitor"
end

class Member
  def greet = "member"
end

class VisitorGreeter < Visitor
  include Greeting
end

class MemberGreeter < Member
  include Greeting
end

p VisitorGreeter.new.greet
p MemberGreeter.new.greet

# defined?(super) guards the end of a chain, &nil withholds the block
module Optional
  def describe = defined?(super) ? "optional+#{super}" : "optional"
end

class Plain
  include Optional
end

class WithParent
  def describe = "parent"
end

class Derived < WithParent
  include Optional
end

p Plain.new.describe
p Derived.new.describe

class Eager
  def each_item(count = 1) = block_given? ? yield(count) : :no_block
end

class Quiet < Eager
  def each_item(count = 2, &) = super(count.succ, &nil)
end

p(Eager.new.each_item { |x| x * 10 })
p(Quiet.new.each_item { |x| x * 10 })

Output:

["Trace", "Email", "Strict", "block"]
[Trace, Email, Strict, Job]
"hello, visitor"
"hello, member"
"optional"
"optional+parent"
10
:no_block

The order ["Trace", "Email", "Strict", "block"] is the ancestor chain [Trace, Email, Strict, Job] read from left to right, with the block reaching the innermost Job#run, which proves that implicit block passing survives all three hops. In the second part the output is "hello, visitor" for one class and "hello, member" for the other. That is how a mixin can add behaviour around whatever method comes next without knowing it, the idea behind Comparable-style protocols, middleware stacks and the decorator pattern discussed in SOLID and Design Patterns.

In the last part, Quiet#each_item calls super(count.succ, &nil) so that Eager#each_item never sees a block and returns :no_block, while the unmodified call yields 10. Without &nil the block would have been passed on silently.

Rules for super

  • super works in instance methods, singleton methods and class methods, and inside blocks within them, where it refers to the enclosing method.

  • It is searched from the owner of the method being executed. Aliasing does not change that: an aliased copy of show that calls super still searches for show, not for the alias name (demonstrated below).

  • Without a successor, super ends in method_missing, whose default raises NoMethodError ("super: no superclass method"). A class that defines method_missing can therefore intercept the call.

  • Inside define_method, bare super is not allowed (the block has no named parameters to forward) and raises RuntimeError; write the arguments out.

  • super in initialize reaches BasicObject#initialize, which takes no arguments: a class that inherits directly from Object and calls bare super with parameters, or super(x), fails with ArgumentError. Use super().

  • Methods that use super in a module and are called on a class that has no successor need defined?(super) or a safe default.

  • Hooks (inherited, included, extended, prepended, method_added) must call super so that other modules in the chain keep working.

  • Data.define and Struct.new instances use super in initialize to set fields: see Struct, Data and Value Objects.

# frozen_string_literal: true

class Root
  def lend(book) = "lent #{book}"
end

class NoParent
  def lend(book) = super(book.strip)
end

class Defined < Root
  define_method(:lend) { |book| super(book.upcase) }
end

class Implicit < Root
  define_method(:lend) { |_book| super }
end

class Fixed < Root
  # BasicObject#initialize takes no arguments, so super() is correct
  def initialize(title)
    @title = title
    super()
  end
end

class Forwarded
  # a bare super passes title on to BasicObject#initialize
  def initialize(title)
    @title = title
    super
  end
end

def attempt
  yield
rescue StandardError => e
  puts "#{e.class}: #{e.message}"
end

attempt { NoParent.new.lend("Dune") }
attempt { puts Defined.new.lend("Dune") }
attempt { Implicit.new.lend("Dune") }
attempt { puts Fixed.new("Dune").class }
attempt { Forwarded.new("Dune") }

Output:

NoMethodError: super: no superclass method 'lend' for an instance of NoParent
lent DUNE
RuntimeError: implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly.
Fixed
ArgumentError: wrong number of arguments (given 1, expected 0)

The five calls give five different outcomes. NoParent#lend calls super but no ancestor defines lend, so Ruby raises NoMethodError with the receiver’s class in the message. Defined calls super(book.upcase) with explicit arguments and works. Implicit uses bare super inside define_method and raises RuntimeError, whose message tells you to specify the arguments. Fixed calls super() in initialize, the idiom for classes whose parent takes no arguments, and Forwarded passes its parameter on implicitly to BasicObject#initialize, which raises ArgumentError. The same failure appears when a class calls bare super from initialize while inheriting from a parent that takes no arguments, a very common cause of "wrong number of arguments" in subclasses.

When nothing is found

method_missing

If the whole chain is searched without a match, Ruby calls method_missing(name, *args, &block) on the receiver, which starts another lookup (for method_missing itself). The default implementation, BasicObject#method_missing, is private and raises: NoMethodError when the call had an explicit receiver (or self.), and NameError ("undefined local variable or method") when the name looked like a variable, that is, a bare identifier with no arguments and no receiver. The exception carries the facts: NameError#name, NameError#receiver, and for NoMethodError also args.

Overriding method_missing gives ghost methods: calls that do not correspond to any def. The rules that keep them from becoming a liability are:

  1. Handle only names you recognise, and call super for everything else, so that real errors are still reported (RuboCop’s Lint/MissingSuper and Style/MissingRespondToMissing check this).

  2. Define respond_to_missing?(name, include_private = false) with the same condition and end it with || super. Then respond_to? and method agree with the ghost methods, so duck-typing checks work.

  3. Forward the whole call: *args, keywords and the block (anonymous *, ** and & parameters can be forwarded since Ruby 3.2).

  4. Prefer defining real methods when the set of names is known; ghost methods are slower than real ones because every call walks the whole chain first. The define_method approach is covered in Dynamic Dispatch and Method Definition.

# frozen_string_literal: true

require "shelf"

# Dynamic finders on top of the catalogue: ghost methods done properly.
class FindableCatalog < Shelf::Catalog
  FINDER = /\Afind_by_(?<field>title|author)\z/

  def method_missing(name, *args, &)
    match = FINDER.match(name)
    return super unless match

    field = match[:field]
    find { |book| field == "title" ? book.title == args.first : book.author.name == args.first }
  end

  def respond_to_missing?(name, include_private = false)
    FINDER.match?(name) || super
  end
end

isbn = Shelf::Isbn.new(digits: "978-0-306-40615-7")
book = Shelf::Book.new(isbn:, title: "Dune", author: Shelf::Author.new(name: "Frank Herbert"))
catalog = FindableCatalog.new(store: Shelf::Store::Memory.new).add(book)

puts catalog.find_by_title("Dune")
puts catalog.find_by_author("Frank Herbert")
p catalog.respond_to?(:find_by_title)
p catalog.respond_to?(:find_by_isbn)
p catalog.method(:find_by_author).call("Nobody")

begin
  catalog.find_by_isbn("x")
rescue NoMethodError => e
  puts e.message
  p e.name, e.args, e.receiver.equal?(catalog)
end

Output:

Dune by Frank Herbert
Dune by Frank Herbert
true
false
nil
undefined method 'find_by_isbn' for an instance of FindableCatalog
:find_by_isbn
["x"]
true

find_by_title and find_by_author do not exist as methods, yet they work, and respond_to? answers true for them and false for find_by_isbn. catalog.method(:find_by_author) returns a Method object because respond_to_missing? vouches for the name; calling it goes through method_missing and returns nil, since no author matches. The unknown find_by_isbn reaches super, which raises the standard NoMethodError, with the name, arguments and receiver intact. If the example omitted return super unless match, that call would silently return nil and hide the typo.

BasicObject proxies

BasicObject is the root of the hierarchy and defines almost nothing: ==, equal?, !, !=, instance_eval, instance_exec, __send__, __id__, and the private initialize, method_missing, singleton_method_added, singleton_method_removed and singleton_method_undefined. Kernel (puts, raise, inspect, class, respond_to?, is_a?) comes in through Object, so a class that inherits from BasicObject has none of it, and references to top-level constants need ::. Such a class is the base for transparent proxies, where every method call must be forwarded and none should be answered by the proxy.

# frozen_string_literal: true

# BasicObject sits above Kernel: a proxy inherits almost nothing, so everything is forwarded.
class Recorder < BasicObject
  def initialize(target)
    @target = target
    @calls = []
  end

  attr_reader :calls

  def method_missing(name, *, &)
    return super unless @target.respond_to?(name)

    @calls << name
    @target.public_send(name, *, &)
  end

  def respond_to_missing?(name, include_private = false) = @target.respond_to?(name, include_private)
end

recorder = Recorder.new([3, 1, 2])
p recorder.sort
p recorder.first
p recorder.calls

begin
  recorder.shuffle!(1, 2, 3)
rescue ArgumentError => e
  puts "#{e.class}: #{e.message}"
end

begin
  recorder.nothing_like_this
rescue NoMethodError => e
  puts e.message
end

# Kernel methods are not reachable from inside a BasicObject subclass
class Blind < BasicObject
  def try_puts = puts("hi")
end

begin
  Blind.new.try_puts
rescue NameError => e
  puts "#{e.class}: #{e.message}"
end

Output:

[1, 2, 3]
3
[:sort, :first]
ArgumentError: wrong number of arguments (given 3, expected 0)
undefined method 'nothing_like_this' for an instance of Recorder
NoMethodError: undefined method 'puts' for an instance of Blind

Source: BasicObject and Kernel

sort and first reach the target through method_missing and are recorded. shuffle!(1, 2, 3) is forwarded as is, and the target raises its own ArgumentError. A name the target does not know goes to super and gives NoMethodError ("for an instance of Recorder"). Blind shows the cost of BasicObject: even puts is a Kernel method and is not found.

Error messages

The text of the lookup errors changed twice, and the messages that older tutorials and Stack Overflow answers quote no longer match what Ruby 4.0 prints. Since Ruby 3.3 the receiver is described by its class instead of by inspect, and since Ruby 3.4 the method name is quoted with apostrophes instead of a backtick and an apostrophe, and a class name prefixes the method name in backtraces.

Situation Ruby 4.0 message Exception

Unknown method on an object

undefined method 'zork' for an instance of S

NoMethodError

Unknown method on nil

undefined method 'zork' for nil

NoMethodError

Unknown class method

undefined method 'zork' for class S

NoMethodError

Bare identifier that is neither a variable nor a method

undefined local variable or method 'zork' for main

NameError

Private method called with a receiver

private method 'sec' called for an instance of T

NoMethodError

Protected method called from outside

protected method 'pr' called for an instance of Sub

NoMethodError

super without a successor

super: no superclass method 'foo' for an instance of S

NoMethodError

undef_method, alias_method or instance_method with an unknown name

undefined method 'x' for class 'Sub'

NameError

remove_method of a method the class does not define

method 'x' not defined in Sub

NameError

BasicObject instance, inspect called

undefined method 'inspect' for an instance of BasicObject

NoMethodError

UnboundMethod#bind to an unrelated object

bind argument must be an instance of Shelf::Catalog

TypeError

Alias chain installed twice

stack level too deep

SystemStackError

NoMethodError is a subclass of NameError, so rescue NameError catches both. NameError and NoMethodError are StandardError`s, but `SystemStackError is not a StandardError (it descends from Exception), so a bare rescue does not catch it; see Exceptions and Error Handling. Production code that matches on message text breaks across versions; match on the class, NameError#name and NameError#receiver instead.

Aliasing, undefining and removing methods

Three operations change which definitions the chain contains, and they differ in a way that matters for lookup.

  • alias new old (keyword) and alias_method :new, :old (method) copy the current definition of old under a second name. The copy is independent: redefining old later does not change new. The keyword takes bare names and is resolved when the class body runs; alias_method takes Symbols or Strings, is a public method since Ruby 2.5, can be called from anywhere (including class_eval or a metaprogramming helper) and returns the new name as a Symbol. The keyword can also alias global variables (alias $new $old), which alias_method cannot.

  • remove_method :name deletes the method from this class only, so a superclass or included module answers again. It raises NameError when this class does not define the method itself.

  • undef_method :name (and the undef keyword) writes a tombstone: the name is blocked for this class and its subclasses even if an ancestor defines it, and a call raises NoMethodError. Module#undefined_instance_methods lists them, and respond_to? returns false.

# frozen_string_literal: true

class Greeter
  def hello = "hello"

  alias howdy hello # the keyword: bare names, resolved when the class body runs
end

Greeter.alias_method(:hi, :hello) # the method: Symbols or Strings, can be called anywhere

# A later redefinition leaves both aliases on the old body.
class Greeter
  def hello = "hello, world" # rubocop:disable Lint/DuplicateMethods
end

greeter = Greeter.new
p greeter.hello, greeter.hi, greeter.howdy
p greeter.method(:hi).name, greeter.method(:hi).original_name
p Greeter.instance_method(:hi) == Greeter.instance_method(:howdy)

class Parent
  def show = "parent show"
end

class Child < Parent
  def show = "child>#{super}"
  alias display show
end

p Child.new.display # super still searches for :show

class Base
  def greet = "base"
end

class Sub < Base
  def greet = "sub"
end

p Sub.send(:alias_method, :old_greet, :greet)
p Sub.send(:remove_method, :greet)
p Sub.new.greet # remove_method lets the superclass answer again

Sub.send(:undef_method, :greet)
begin
  Sub.new.greet
rescue NoMethodError => e
  puts e.message
end
p Sub.undefined_instance_methods
p Sub.new.respond_to?(:greet), Sub.method_defined?(:greet)

Output:

"hello, world"
"hello"
"hello"
:hi
:hello
true
"child>parent show"
:old_greet
Sub
"base"
undefined method 'greet' for an instance of Sub
[:greet]
false
false

greeter.hello returns the new body while hi and howdy keep the original: aliases are snapshots. method(:hi).original_name is :hello, and the two aliases compare equal as UnboundMethod`s because they share one definition. In `Child, display is an alias of a method that calls super, and it still reaches Parent#show, which shows that super follows the name in the original definition. remove_method returns the class and brings Base#greet back, whereas undef_method makes the call fail even though Base defines it. alias_method on a method that does not exist raises NameError, as does instance_method.

Module#method_added, method_removed and method_undefined (and the singleton versions) fire for these operations; as always, they must call super.

Wrapping a method

Adding behaviour before or after an existing method is the most common reason to touch the chain. There are three techniques, and only one is recommended in current code.

  • Alias chain (the historical "alias_method_chain"): alias the original to a new name, define the wrapper under the old name, and call the alias. It works on any class and needs no module, but it is not idempotent. If the file that installs the wrapper is loaded twice, the second alias captures the wrapper, the wrapper calls the alias, and the call never ends.

  • Prepend: write a module that defines the method and calls super, then prepend it. The original stays reachable through super, the module can be reused on other classes, Method#super_method shows the original, and a second prepend of the same module is a no-op.

  • Refinements (refine and using) confine the change to a file or scope; see Refinements and Monkey Patching.

The alias chain first. The wrapper below is installed twice, which is what happens when a patch file is required from two places or reloaded in development.

# frozen_string_literal: true

require "shelf"

# The old way: an alias chain, done twice by accident, never terminates.
module Shelf
  class Catalog
    def self.log_adds
      alias_method :add_without_log, :add

      define_method(:add) do |book|
        add_without_log(book) # now calls this very method
      end
    end
  end
end

isbn = Shelf::Isbn.new(digits: "978-0-306-40615-7")
book = Shelf::Book.new(isbn:, title: "Dune", author: Shelf::Author.new(name: "Frank Herbert"))

Shelf::Catalog.log_adds
Shelf::Catalog.log_adds # loaded twice: add_without_log now points at the wrapper

begin
  Shelf::Catalog.new.add(book)
rescue SystemStackError => e
  puts "#{e.class}: #{e.message}"
end

Output:

SystemStackError: stack level too deep

The first call to log_adds aliases the real add to add_without_log and installs the wrapper. The second call aliases the wrapper to add_without_log and installs a new wrapper, so add_without_log now points to a method that calls add_without_log. The first call of add recurses until the stack is exhausted. The same program with prepend is stable.

# frozen_string_literal: true

require "shelf"

# The modern way: a prepended module, reusable and removable from the chain only by not prepending it.
module AddLogging
  def add(book)
    puts "adding #{book.title}"
    super
  end
end

Shelf::Catalog.prepend(AddLogging)
Shelf::Catalog.prepend(AddLogging) # the second prepend changes nothing

isbn = Shelf::Isbn.new(digits: "978-0-306-40615-7")
book = Shelf::Book.new(isbn:, title: "Dune", author: Shelf::Author.new(name: "Frank Herbert"))
catalog = Shelf::Catalog.new(store: Shelf::Store::Memory.new)

p catalog.add(book).equal?(catalog)
p catalog.count
p Shelf::Catalog.ancestors.first(3)
p catalog.method(:add).owner
p catalog.method(:add).super_method.owner
p catalog.method(:add).super_method.source_location.last

Output:

adding Dune
true
1
[AddLogging, Shelf::Catalog, Enumerable]
AddLogging
Shelf::Catalog
13

The wrapper ran once even though prepend was called twice, ancestors shows AddLogging in front of Shelf::Catalog, and super_method leads to the original Catalog#add at line 13 of the library. Because super passes the arguments and block, the wrapper does not need to repeat the signature. Prepending to a class that already has live instances affects them immediately.

The remaining differences between the techniques:

Aspect Alias chain Prepend Refinement

Idempotent

No, a second installation recurses.

Yes, a module is added to the chain once.

Yes.

Visible to other files

Everywhere, immediately.

Everywhere, immediately.

Only where using is active.

Original reachable as

The alias name.

super.

super (the refined class).

Introspection

original_name of the alias.

ancestors, super_method.

Module#refinements, Refinement#target.

RuboCop

Style/Alias prefers alias in a class body.

Lint/MissingSuper checks hooks.

None specific.

Redefining a method in place (class String; def to_s ...) is the monkey-patching case: it replaces the original with no way to call it, and with -w it emits "method redefined; discarding old to_s". Prefer prepend when the behaviour must be global, and a refinement when it must be local.

Pitfalls

  • Looking at the class, not the chain. Shelved.instance_methods(false) lists only what the class itself defines; Shelved.instance_method(:x).owner names where the answer lives.

  • Prepended methods hide the class’s own. A prepended module is searched first, so Method#owner for a method the class defines is the module, and tools that match on the class (source_location) point at the wrapper.

  • Bare super after changing a parameter sends the changed value. Use super() or explicit arguments when that is not what you want.

  • super in a module included in two unrelated classes reaches different methods; test the module in the classes it is mixed into.

  • Methods defined in a block with define_method need explicit super(args) and cannot use bare super.

  • Shared instance variables. Every module in the chain sees the receiver’s instance variables, so two mixins that both use @cache collide; prefix the names.

  • method_missing without respond_to_missing? breaks respond_to?, method and Array()-style conversions that probe with respond_to?.

  • method_missing that swallows errors. Not calling super for unknown names converts typos into nil.

  • Private calls reach method_missing. Calling a private or protected method with an explicit receiver is dispatched to method_missing, so a ghost-method handler without super hides "private method called" errors.

  • Modifying Object or Kernel changes every lookup in the process.

  • Included modules come before Object. A method the class defines wins; one it does not define comes from the module, and a module method wins over one with the same name in Object, Kernel or BasicObject (for example Comparable#== over BasicObject#==).

  • Calling super from a singleton method needs a method of that name in the singleton chain, not in the class chain.

Where older material is outdated

  • Alias chains (alias_method_chain, a Rails-era helper, and hand-written equivalents) are the idiom of the 1.8 and 1.9 era. Modern code prepends a module.

  • Module#include only affecting future includers is the pre-3.0 behaviour described above; since Ruby 3.0 later changes propagate.

  • method_missing examples without respond_to_missing? appear in many older tutorials; respond_to_missing? has been the expected companion since Ruby 1.9.2. Current code defines both and ends both with super.

  • Bare super in a hook or constructor that passes arguments the parent does not accept is a recurring bug in published examples; check that the parent’s signature accepts what the bare form forwards, or use super().

  • Hooks without super such as inherited and method_added silence other modules' hooks.

  • Error text. Messages with a backtick as the opening quote are from Ruby 3.3 or earlier, and messages that print the receiver as #<Foo:0x...> are from Ruby 3.2 or earlier.

  • Method#unbind / bind everywhere. bind_call (Ruby 2.7) replaces the two-step form.

  • singleton_class.send(:define_method, ...) workarounds are obsolete since define_singleton_method and the public define_method (Ruby 2.5).

RuboCop cops for lookup and super

The following cops, all available in RuboCop 1.92, encode the rules of this page.

Cop Rule it enforces

Lint/MissingSuper

Constructors and lifecycle hooks (initialize, inherited, included, method_added and others) must call super.

Style/MissingRespondToMissing

A class that defines method_missing must also define respond_to_missing?.

Lint/UselessMethodDefinition

A method that only calls super with the same arguments is useless.

Style/SuperArguments

Call super without arguments when they repeat the signature.

Style/Alias

Use alias in a class body and alias_method elsewhere.

Lint/DuplicateMethods

The same method defined twice in one class body.

Style/MixinGrouping

One include per statement in a class body.

References