Ruby method_missing — Ghost Methods Dynamic Proxies and Delegation Explained
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