Skip to main content
โšก Calmops

method_missing in Ruby: Ghost Methods and Dynamic Dispatch

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:

  1. Search the object’s class and its ancestors for the method
  2. If not found, call method_missing on the object
  3. Default method_missing raises NoMethodError
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_missing intercepts calls to undefined methods
  • Always pair it with respond_to_missing? for correct introspection
  • Always call super for unhandled method names
  • Use it for DSLs, proxies, dynamic attributes, and delegation
  • For performance-critical paths, use define_method to cache the method after first call

Resources

Comments