Skip to content

Ruby method_missing — Ghost Methods Dynamic Proxies and Delegation Explained

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Ruby method_missing. We cover key concepts, practical examples, and best practices to help you master this topic.

Ruby method_missing intercepts calls to undefined methods on an object, enabling ghost methods that don't exist until called, dynamic proxies for delegation, and automatic respond_to? integration.

What You'll Learn

  • Implementing method_missing for ghost methods
  • Building delegation proxies
  • Using respond_to_missing? correctly
  • Performance and debugging considerations

Why It Matters

method_missing powers Rails' dynamic finders, ActiveRecord associations, and OpenStruct. Doda Browser uses method_missing for configuration proxies. Durga Antivirus Pro uses it for plugin dispatch. It's essential for advanced Metaprogramming.

Real-World Use

OpenStruct, ActiveSupport's HashWithIndifferentAccess, Rails route helpers, RSpec's should/expect syntax, and delegation patterns all rely on method_missing.

flowchart LR
    A["method_missing"] --> B["Ghost Methods"]
    B --> C["Proxies"]
    C --> D["Delegation"]
    D --> E["respond_to_missing?"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#dbeafe,stroke:#2563eb,color:#1e40af

Basic method_missing

class Ghost
  def method_missing(name, *args, &block)
    "You called #{name} with #{args.inspect}"
  end
end

g = Ghost.new
puts g.hello                 # You called hello with []
puts g.greet("Alice")        # You called greet with ["Alice"]
puts g.calculate(1, 2, 3)    # You called calculate with [1, 2, 3]

Ghost Methods

Methods that don't exist as defined methods but respond to calls:

class DynamicConfig
  def initialize
    @settings = {}
  end

  def method_missing(name, *args)
    key = name.to_s

    if key.end_with?("=")
      @settings[key.chop] = args.first
    elsif @settings.key?(key)
      @settings[key]
    else
      super  # Raise NoMethodError for truly unknown methods
    end
  end

  def respond_to_missing?(name, include_private = false)
    key = name.to_s
    @settings.key?(key) || key.end_with?("=") || super
  end
end

config = DynamicConfig.new
config.database_url = "postgres://localhost/mydb"
config.port = 5432

puts config.database_url  # postgres://localhost/mydb
puts config.port           # 5432
puts config.respond_to?(:database_url)  # true
# puts config.unknown     # NoMethodError

Proxy/Delegate Pattern

class Proxy
  def initialize(target)
    @target = target
  end

  def method_missing(name, *args, &block)
    if @target.respond_to?(name)
      log_call(name, args)
      @target.send(name, *args, &block)
    else
      super
    end
  end

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

  private

  def log_call(name, args)
    puts "[PROXY] #{name}(#{args.join(', ')})"
  end
end

class Calculator
  def add(a, b) = a + b
  def multiply(a, b) = a * b
end

calc = Proxy.new(Calculator.new)
puts calc.add(5, 3)         # [PROXY] add(5, 3) \n 8
puts calc.multiply(4, 7)    # [PROXY] multiply(4, 7) \n 28
puts calc.respond_to?(:add) # true

ActiveRecord-style Dynamic Finders

class RecordCollection
  def initialize
    @records = []
  end

  def add(record)
    @records << record
  end

  def method_missing(name, *args)
    if name.to_s =~ /^find_by_(\w+)$/
      field = $1
      @records.select { |r| r[field.to_sym] == args.first }
    elsif name.to_s =~ /^find_all_by_(\w+)$/
      field = $1
      @records.select { |r| r[field.to_sym] == args.first }
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s.start_with?("find_by_", "find_all_by_") || super
  end
end

users = RecordCollection.new
users.add({ name: "Alice", role: "admin", age: 30 })
users.add({ name: "Bob", role: "user", age: 25 })
users.add({ name: "Charlie", role: "admin", age: 35 })

puts users.find_by_name("Alice").inspect
puts users.find_all_by_role("admin").size  # 2

Forwardable Alternative

require "forwardable"

class LoggerProxy
  extend Forwardable

  def_delegators :@logger, :info, :warn, :error, :fatal

  def initialize(logger)
    @logger = logger
  end

  def format_message(level, msg)
    "[#{Time.now.utc.iso8601}] #{level}: #{msg}"
  end

  def method_missing(name, *args, &block)
    if @logger.respond_to?(name)
      puts "[LOG] #{name}"
      @logger.send(name, *args, &block)
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @logger.respond_to?(name) || super
  end
end

Chain of method_missing

class SafeNavigator
  def initialize(obj)
    @obj = obj
  end

  def method_missing(name, *args)
    if @obj.respond_to?(name)
      result = @obj.send(name, *args)
      if result.nil?
        SafeNavigator.new(nil)
      else
        SafeNavigator.new(result)
      end
    elsif @obj.nil?
      SafeNavigator.new(nil)
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @obj&.respond_to?(name) || super
  end

  def to_s
    @obj.to_s
  end

  def nil?
    @obj.nil?
  end
end

# Simulates Ruby 2.3+ &. operator
data = { user: { name: "Alice", address: { city: "NYC" } } }
navigator = SafeNavigator.new(data)

city = navigator.user.address.city
puts city  # NYC

missing = navigator.user.non_existent.anything
puts missing.nil?  # true

Method Registration Pattern

class MethodRouter
  def initialize
    @routes = {}
  end

  def on(pattern, &handler)
    @routes[pattern] = handler
  end

  def method_missing(name, *args, &block)
    @routes.each do |pattern, handler|
      if name.to_s.match(pattern)
        return handler.call(name, *args, &block)
      end
    end
    super
  end

  def respond_to_missing?(name, include_private = false)
    @routes.keys.any? { |p| name.to_s.match(p) } || super
  end
end

router = MethodRouter.new
router.on(/^find_\w+/) do |name, *args|
  "Finding: #{name}(#{args.join(', ')})"
end
router.on(/^create_\w+/) do |name, *args|
  "Creating: #{name}(#{args.join(', ')})"
end

puts router.find_user(1)      # Finding: find_user(1)
puts router.create_order("A") # Creating: create_order(A)

Common Mistakes

1. Not Calling super for Unhandled Methods

class BadProxy
  def method_missing(name, *args)
    # Should call super for unhandled methods
    "handled"
  end
end

p = BadProxy.new
p.anything  # "handled" — silently catches all
p.object_id # "handled" — even real methods!

2. Forgetting respond_to_missing?

class BadGhost
  def method_missing(name, *args)
    "handled"
  end
end

g = BadGhost.new
puts g.respond_to?(:hello)  # false — breaks duck typing!

3. Infinite Recursion

class Infinite
  def method_missing(name, *args)
    send(name, *args)  # Calls method_missing again!
  end
end

4. Performance Issues in Hot Paths

# method_missing is ~10x slower than real methods
# For frequently called ghost methods, use define_method instead

# Better — define once with define_method
%w[name email phone].each do |field|
  define_method(field) { @data[field] }
end

5. Overriding method_missing When Delegation Is Simpler

# Complex — unnecessary
class Wrapper
  def method_missing(name, *args)
    @target.send(name, *args)
  end
end

# Simple — use Forwardable
require "forwardable"
class Wrapper
  extend Forwardable
  def_delegators :@target, :method1, :method2, :method3
end

Practice Questions

1. What is method_missing?

A hook that Ruby calls when an object receives a method that isn't defined. Override it to handle calls to undefined methods dynamically.

2. Why must you override respond_to_missing? with method_missing?

To maintain correct behavior of respond_to?. Without it, ghost methods return false for respond_to?, breaking duck typing and Ruby's type introspection.

3. What's the difference between method_missing and define_method?

method_missing intercepts calls to non-existent methods at call time. define_method creates actual methods at class load time. define_method is faster but requires knowing method names in advance.

4. When should you NOT use method_missing?

When method names are known in advance (use define_method), when performance matters, when you need precise error messages, or when simple delegation suffices.

Challenge: Build a HashAccessor class that creates getter/setter methods via method_missing for any hash key, with nested hash support.

Solution
class HashAccessor
  def initialize(data = {})
    @data = deep_convert(data)
  end

  def method_missing(name, *args)
    key = name.to_s

    if key.end_with?("=")
      @data[key.chop] = args.first
    elsif @data.key?(key)
      value = @data[key]
      if value.is_a?(Hash)
        HashAccessor.new(value)
      else
        value
      end
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    key = name.to_s
    @data.key?(key) || key.end_with?("=") || super
  end

  def to_h
    @data
  end

  private

  def deep_convert(obj)
    case obj
    when Hash
      obj.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_convert(v) }
    when Array
      obj.map { |e| deep_convert(e) }
    else
      obj
    end
  end
end

config = HashAccessor.new({
  database: {
    host: "localhost",
    port: 5432,
    credentials: { user: "admin", password: "secret" }
  },
  app_name: "MyApp"
})

puts config.app_name              # MyApp
puts config.database.host         # localhost
puts config.database.port         # 5432
puts config.database.credentials.user  # admin
puts config.database.respond_to?(:host)  # true

FAQ

{{< faq question="Is method_missing slow?" >}} Yes. Every call triggers a failed method lookup first, which is about 10x slower than a real method. For frequently called ghost methods, use define_method instead after the first call or at load time. {{< /faq >}}

{{< faq question="What's the difference between method_missing and const_missing?" >}} method_missing handles undefined instance methods. const_missing handles undefined constants. Both follow the same pattern — override to handle dynamically, call super otherwise. {{< /faq >}}

{{< faq question="Can I use method_missing with a whitelist?" >}} Yes. Override method_missing, check if the method is in a whitelist, call super otherwise. This ensures only intended methods are handled dynamically and real errors aren't swallowed. {{< /faq >}}

{{< faq question="How do I test method_missing?" >} Test that the ghost method works: assert_equal "Alice", config.name. Test respond_to?: assert config.respond_to?(:name). Test unhandled methods raise NoMethodError: assert_raises(NoMethodError) { config.unknown }. {{< /faq >}}

{{< faq question="What happens if I define a real method that method_missing would handle?" >} The real method takes priority. Ruby checks defined methods before calling method_missing. You can define performance-critical methods as real methods and use method_missing as fallback. {{< /faq >}}

Try It Yourself

# method_missing_demo.rb

class TranslationProxy
  def initialize(language)
    @language = language
    @translations = {
      en: { hello: "Hello", goodbye: "Goodbye" },
      es: { hello: "Hola", goodbye: "Adios" },
      fr: { hello: "Bonjour", goodbye: "Au revoir" }
    }
  end

  def method_missing(name, *args)
    word = @translations[@language]&.dig(name)
    if word
      word
    elsif @translations.values.any? { |t| t.key?(name) }
      "[#{@language} translation not available]"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @translations.values.any? { |t| t.key?(name) } || super
  end
end

en = TranslationProxy.new(:en)
es = TranslationProxy.new(:es)
fr = TranslationProxy.new(:fr)

puts en.hello  # Hello
puts es.hello  # Hola
puts fr.hello  # Bonjour
puts es.goodbye # Adios
puts en.respond_to?(:hello)   # true
puts en.respond_to?(:unknown) # false

What's Next

Now that you understand method_missing, explore const_missing for dynamic constant resolution.

Topic Description Link
Ruby const_missing Dynamic constant resolution {{< ref "37-const-missing" >}}
Ruby Metaprogramming define_method, send {{< ref "33-metaprogramming-basics" >}}
Python getattr Compare Python's method interception Python

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro