Understanding Ruby's Method Lookup Path
You call a method on an object. Ruby finds it and runs it. Simple, right? Except sometimes Ruby finds the wrong one. Or it finds nothing and you get a NoMethodError on something you’re sure should work. Or a module you included seems to be ignored entirely.
When these bugs show up, and they will, understanding Ruby’s method lookup path is the difference between debugging for five minutes and debugging for an hour.
The basic chain
When you call object.some_method, Ruby follows a specific, deterministic path to find the method definition. Here’s the order:
- The object’s singleton class (methods defined on that specific object)
- The object’s class
- Modules included in that class (last included wins)
- The superclass
- Modules included in the superclass
- Up the chain until
BasicObject - If still not found, start
method_missingfrom the bottom again
This isn’t abstract theory. You can see the exact chain for any object:
class Animal
def speak
"..."
end
end
module Trainable
def sit
"sitting"
end
end
module Domesticated
def loyal?
true
end
end
class Dog < Animal
include Trainable
include Domesticated
def fetch
"fetching"
end
end
Dog.ancestors
# => [Dog, Domesticated, Trainable, Animal, Object, Kernel, BasicObject]
That array is the method lookup path. When you call a method on a Dog instance, Ruby walks that list from left to right, checking each entry for a matching method definition. First match wins.
Last included wins
Notice the order of the modules: Domesticated comes before Trainable in the ancestors chain, even though Trainable was included first. This is because Ruby inserts included modules directly above the including class, and later includes go higher in the stack.
This matters when two modules define the same method:
module A
def greet
"hello from A"
end
end
module B
def greet
"hello from B"
end
end
class MyClass
include A
include B
end
MyClass.new.greet # => "hello from B"
MyClass.ancestors # => [MyClass, B, A, Object, Kernel, BasicObject]
B was included last, so it appears first in the ancestor chain after MyClass. Its greet gets found first. If you expected A’s version, you’d be confused, until you check .ancestors.
Prepend: the one that goes in front
include inserts a module after the class in the lookup chain. prepend inserts it before.
module Logging
def save
puts "about to save..."
super
puts "saved!"
end
end
class Record
prepend Logging
def save
puts "saving record"
end
end
Record.ancestors
# => [Logging, Record, Object, Kernel, BasicObject]
Record.new.save
# about to save...
# saving record
# saved!
Logging comes before Record in the chain. So when you call save, Ruby finds Logging#save first. The super call inside it then continues up the chain to Record#save.
This is incredibly useful for wrapping behavior around existing methods without modifying the original class. It’s how many gems add instrumentation, caching, or validation around your methods. And it’s why prepend largely replaced the old alias_method_chain hack.
Singleton methods: the per-object override
Ruby lets you define methods on individual objects, not just classes:
dog = Dog.new
def dog.tricks
"roll over"
end
Where does this method live? In the object’s singleton class (also called its eigenclass or metaclass). The singleton class sits at the very front of the lookup chain, before even the object’s actual class.
dog.singleton_class.ancestors
# => [#<Class:#<Dog:0x...>>, Dog, Domesticated, Trainable, Animal, Object, Kernel, BasicObject]
That #<Class:#<Dog:0x...>> at the front is the singleton class. Any method defined there takes priority over everything else. This is also how class methods work, they’re singleton methods on the class object itself.
Where method_missing fits
If Ruby walks the entire ancestor chain and finds nothing, it doesn’t give up immediately. It starts over from the bottom, this time looking for method_missing. If any class or module in the chain defines method_missing, that gets called with the method name and arguments.
class FlexibleConfig
def method_missing(name, *args)
if name.to_s.end_with?("=")
instance_variable_set("@#{name.to_s.chomp('=')}", args.first)
else
instance_variable_get("@#{name}")
end
end
def respond_to_missing?(name, include_private = false)
true
end
end
config = FlexibleConfig.new
config.database = "postgres"
config.database # => "postgres"
This is powerful but dangerous. If you define method_missing without also defining respond_to_missing?, introspection breaks. And if you forget to call super for methods you don’t handle, you’ll swallow errors silently.
The rule of thumb: avoid method_missing unless you’re building a DSL or proxy object, and always define respond_to_missing? alongside it.
Debugging with ancestors
When you hit a “wrong method” bug, .ancestors is your best friend. Here’s the workflow:
# What class is this object?
object.class
# => SomeClass
# What's the full lookup chain?
object.class.ancestors
# => [SomeClass, ModuleB, ModuleA, ParentClass, Object, Kernel, BasicObject]
# Where is this specific method defined?
object.method(:problematic_method).owner
# => ModuleA
# What's the source location?
object.method(:problematic_method).source_location
# => ["/path/to/file.rb", 42]
That last one, source_location, is gold. It tells you exactly which file and line number defines the method Ruby is actually calling. When a method behaves unexpectedly, this is the fastest way to find out if you’re even looking at the right definition.
The practical takeaway
You don’t need to think about method lookup on every method call. But when something goes wrong, when a method returns unexpected results, when an override doesn’t seem to take effect, when super calls the wrong parent, this is the mental model that gets you unstuck.
The rules are simple and consistent: singleton class first, then class, then modules (last included first), then superclass, repeat. When in doubt, print .ancestors and read it left to right. The answer is always in that list.
Comparing complex Ruby objects and the diff is hard to read? Try RubyHash, paste your expected and actual output for a clean, highlighted comparison.
Enjoyed this post?
Subscribe to get notified when we publish more Ruby and Rails content.