Introduction
When you call a method that doesn’t exist on a Ruby object, Ruby doesn’t immediately raise a NoMethodError. Instead, it calls a special hook method: method_missing. By overriding this hook, you can intercept undefined method calls and handle them dynamically โ a technique known as ghost methods.
This is one of Ruby’s most powerful metaprogramming tools, used extensively in Rails (ActiveRecord, dynamic finders), RSpec, and many DSL libraries.
How method_missing Works
Ruby’s method lookup process:
- Search the object’s class and its ancestors for the method
- If not found, call
method_missingon the object - Default
method_missingraisesNoMethodError
class Ghost
def method_missing(name, *args)
puts "You called: #{name} with args: #{args.inspect}"
"ghost response"
end
end
g = Ghost.new
g.anything # => You called: anything with args: []
g.foo(1, 2, 3) # => You called: foo with args: [1, 2, 3]
g.bar("hello") # => You called: bar with args: ["hello"]
A Practical Example: Dynamic Attributes
class Student
attr_accessor :name
attr_accessor :age
def initialize(name, age)
@name = name
@age = age
end
def method_missing(name, *args)
if name.to_s =~ /test1/
return "test1"
end
"a ghost method: #{name}"
end
end
s1 = Student.new("lily", 25)
puts s1.name # => "lily" (real method)
puts s1.test1 # => "test1" (ghost method)
puts s1.test2 # => "a ghost method: test2"
Always Override respond_to_missing?
When you implement method_missing, you must also override respond_to_missing?. Otherwise, respond_to? returns false for your ghost methods, breaking duck typing and introspection:
class DynamicProxy
def initialize(target)
@target = target
end
def method_missing(name, *args, &block)
if @target.respond_to?(name)
@target.send(name, *args, &block)
else
super # important: call super for unhandled methods
end
end
def respond_to_missing?(name, include_private = false)
@target.respond_to?(name, include_private) || super
end
end
proxy = DynamicProxy.new("hello")
puts proxy.upcase # => "HELLO"
puts proxy.length # => 5
puts proxy.respond_to?(:upcase) # => true (works because of respond_to_missing?)
Real-World Use Case: OpenStruct-like Dynamic Attributes
class FlexibleRecord
def initialize(attributes = {})
@attributes = attributes
end
def method_missing(name, *args)
attr_name = name.to_s
if attr_name.end_with?('=')
# Setter: record.name = "Alice"
@attributes[attr_name.chomp('=')] = args.first
elsif @attributes.key?(attr_name)
# Getter: record.name
@attributes[attr_name]
else
super
end
end
def respond_to_missing?(name, include_private = false)
attr_name = name.to_s.chomp('=')
@attributes.key?(attr_name) || super
end
def to_h
@attributes.dup
end
end
record = FlexibleRecord.new
record.name = "Alice"
record.email = "[email protected]"
record.age = 30
puts record.name # => "Alice"
puts record.email # => "[email protected]"
puts record.age # => 30
puts record.to_h.inspect
# => {"name"=>"Alice", "email"=>"[email protected]", "age"=>30}
Use Case: Method Delegation
class Decorator
def initialize(component)
@component = component
end
# Delegate all unknown methods to the wrapped component
def method_missing(name, *args, &block)
if @component.respond_to?(name)
@component.send(name, *args, &block)
else
super
end
end
def respond_to_missing?(name, include_private = false)
@component.respond_to?(name, include_private) || super
end
end
class Logger < Decorator
def save
puts "[LOG] Saving..."
result = super # delegates to @component.save via method_missing
puts "[LOG] Saved."
result
end
end
class Document
def save
puts "Document saved."
true
end
def title
"My Document"
end
end
doc = Logger.new(Document.new)
doc.save # => [LOG] Saving... / Document saved. / [LOG] Saved.
doc.title # => "My Document" (delegated via method_missing)
Use Case: DSL Builder
class HtmlBuilder
def initialize
@html = ""
end
def method_missing(tag, content = nil, **attrs, &block)
attr_str = attrs.map { |k, v| " #{k}=\"#{v}\"" }.join
if block
@html += "<#{tag}#{attr_str}>"
instance_eval(&block)
@html += "</#{tag}>"
else
@html += "<#{tag}#{attr_str}>#{content}</#{tag}>"
end
self
end
def respond_to_missing?(name, include_private = false)
true # all method names are valid HTML tags
end
def to_s
@html
end
end
html = HtmlBuilder.new
html.div(class: "container") do
html.h1("Hello World")
html.p("This is a paragraph.", class: "text")
end
puts html.to_s
# => <div class="container"><h1>Hello World</h1><p class="text">This is a paragraph.</p></div>
Use Case: ActiveRecord-style Dynamic Finders
Rails uses method_missing to implement find_by_* methods:
class Model
COLUMNS = [:name, :email, :age]
def self.method_missing(name, *args)
if name.to_s.start_with?('find_by_')
column = name.to_s.sub('find_by_', '').to_sym
if COLUMNS.include?(column)
find_by(column => args.first)
else
super
end
else
super
end
end
def self.respond_to_missing?(name, include_private = false)
name.to_s.start_with?('find_by_') || super
end
def self.find_by(conditions)
puts "SELECT * WHERE #{conditions.inspect} LIMIT 1"
end
end
Model.find_by_name("Alice") # => SELECT * WHERE {:name=>"Alice"} LIMIT 1
Model.find_by_email("[email protected]") # => SELECT * WHERE {:email=>"[email protected]"} LIMIT 1
Model.respond_to?(:find_by_name) # => true
Performance Considerations
method_missing is slower than regular method calls because Ruby has to walk the entire ancestor chain before calling it. For performance-critical code, consider using define_method to create real methods on first call:
class FastDynamic
def method_missing(name, *args)
if name.to_s.start_with?('compute_')
# Define the method for future calls (avoids method_missing overhead)
self.class.define_method(name) do |*a|
"computed: #{name}"
end
send(name, *args)
else
super
end
end
def respond_to_missing?(name, include_private = false)
name.to_s.start_with?('compute_') || super
end
end
obj = FastDynamic.new
obj.compute_total # first call: goes through method_missing, defines method
obj.compute_total # subsequent calls: uses the defined method directly
Common Pitfalls
Forgetting super
Always call super for unhandled cases, otherwise you swallow all NoMethodErrors:
# Bad: swallows all errors
def method_missing(name, *args)
"handled"
end
# Good: only handle what you intend to
def method_missing(name, *args)
if name.to_s.start_with?('my_prefix_')
# handle it
else
super # let Ruby raise NoMethodError for everything else
end
end
Forgetting respond_to_missing?
# Bad: respond_to? lies
class Bad
def method_missing(name, *args)
"ghost"
end
end
Bad.new.respond_to?(:anything) # => false (wrong!)
# Good
class Good
def method_missing(name, *args)
"ghost"
end
def respond_to_missing?(name, include_private = false)
true
end
end
Good.new.respond_to?(:anything) # => true
Summary
method_missingintercepts calls to undefined methods- Always pair it with
respond_to_missing?for correct introspection - Always call
superfor unhandled method names - Use it for DSLs, proxies, dynamic attributes, and delegation
- For performance-critical paths, use
define_methodto cache the method after first call
Resources
- Ruby Docs: BasicObject#method_missing
- Ruby Docs: Module#respond_to_missing?
- Metaprogramming Ruby (Paolo Perrotta)
Comments