Ruby's method_missing mechanism for intercepting undefined method calls and implementing dynamic behavior
Overview
Ruby's method_missing
is a callback method that Ruby invokes whenever an object receives a message for an undefined method. This mechanism forms the foundation for dynamic method dispatch, domain-specific languages, and proxy objects. When Ruby cannot find a method definition through its normal lookup chain, it calls method_missing
on the receiving object before raising a NoMethodError
.
Every Ruby object inherits method_missing
from BasicObject
, which raises NoMethodError
by default. Classes can override this method to implement custom behavior for undefined method calls. The method receives the method name as the first argument, followed by any arguments and block passed to the original call.
class DynamicHandler
def method_missing(method_name, *args, **kwargs, &block)
puts "Called #{method_name} with #{args.inspect}"
puts "Keyword arguments: #{kwargs.inspect}" unless kwargs.empty?
puts "Block given: #{block_given?}"
end
end
handler = DynamicHandler.new
handler.undefined_method(1, 2, name: "test") { puts "block" }
# Called undefined_method with [1, 2]
# Keyword arguments: {:name=>"test"}
# Block given: true
The method_missing
hook operates after Ruby's standard method resolution process completes. Ruby first searches the object's singleton class, then the class hierarchy including modules, before falling back to method_missing
. This makes it a catch-all mechanism rather than a replacement for normal method dispatch.
class Example
def existing_method
"found"
end
def method_missing(method_name, *args)
"missing: #{method_name}"
end
end
obj = Example.new
obj.existing_method # => "found"
obj.nonexistent_method # => "missing: nonexistent_method"
Basic Usage
Implementing method_missing
requires understanding its method signature and Ruby's expectations. The method receives the undefined method name as a symbol, followed by arguments, keyword arguments, and an optional block. Most implementations examine the method name to decide how to respond.
class ConfigObject
def initialize
@data = {}
end
def method_missing(method_name, *args, **kwargs, &block)
key = method_name.to_s
if key.end_with?('=')
# Setter method
@data[key.chomp('=')] = args.first
elsif args.empty? && kwargs.empty?
# Getter method
@data[key]
else
super
end
end
end
config = ConfigObject.new
config.database_url = "postgres://localhost/mydb"
config.port = 5432
puts config.database_url # => "postgres://localhost/mydb"
puts config.port # => 5432
When implementing method_missing
, also define respond_to_missing?
to maintain Ruby's introspection contract. This method should return true for method names that method_missing
handles, ensuring that respond_to?
works correctly.
class AttributeHash
def initialize
@attributes = {}
end
def method_missing(method_name, *args)
name = method_name.to_s
if name.end_with?('=')
@attributes[name.chomp('=')] = args.first
elsif args.empty?
@attributes[name]
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
name = method_name.to_s
name.end_with?('=') || @attributes.key?(name) || super
end
end
hash = AttributeHash.new
hash.respond_to?(:name) # => false
hash.name = "test"
hash.respond_to?(:name) # => true
hash.respond_to?(:name=) # => true
Always call super
when method_missing
cannot handle a method call. This ensures that Ruby's standard error handling occurs for truly undefined methods, maintaining expected behavior and error messages.
class SelectiveHandler
HANDLED_PREFIXES = %w[get_ set_ find_].freeze
def method_missing(method_name, *args)
name = method_name.to_s
if HANDLED_PREFIXES.any? { |prefix| name.start_with?(prefix) }
handle_dynamic_method(name, args)
else
super # Let Ruby handle unrecognized methods
end
end
private
def handle_dynamic_method(name, args)
"Handled #{name} with #{args.inspect}"
end
end
Advanced Usage
Complex method_missing
implementations often involve method name parsing, delegation patterns, and integration with Ruby's module system. These patterns enable sophisticated dynamic behavior while maintaining reasonable performance characteristics.
Method name parsing allows creating mini-DSLs within Ruby classes. Extract meaningful information from method names to drive behavior, using regular expressions or string analysis to decode intentions.
class QueryBuilder
def initialize(table_name)
@table = table_name
@conditions = []
@ordering = []
end
def method_missing(method_name, *args)
name = method_name.to_s
case name
when /^find_by_(.+)$/
column = $1
@conditions << "#{column} = #{quote_value(args.first)}"
self
when /^order_by_(.+)_(asc|desc)$/
column, direction = $1, $2
@ordering << "#{column} #{direction.upcase}"
self
when /^limit_(\d+)$/
@limit = $1.to_i
self
else
super
end
end
def to_sql
sql = "SELECT * FROM #{@table}"
sql += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
sql += " ORDER BY #{@ordering.join(', ')}" unless @ordering.empty?
sql += " LIMIT #{@limit}" if @limit
sql
end
private
def quote_value(value)
case value
when String then "'#{value}'"
when Numeric then value.to_s
else "'#{value}'"
end
end
end
query = QueryBuilder.new("users")
.find_by_status("active")
.find_by_role("admin")
.order_by_created_at_desc
.limit_10
puts query.to_sql
# => SELECT * FROM users WHERE status = 'active' AND role = 'admin' ORDER BY created_at DESC LIMIT 10
Delegation through method_missing
creates proxy objects that forward method calls to other objects while adding cross-cutting concerns like logging, caching, or access control.
class LoggingProxy
def initialize(target, logger = Logger.new(STDOUT))
@target = target
@logger = logger
end
def method_missing(method_name, *args, **kwargs, &block)
start_time = Time.now
@logger.info("Calling #{method_name} on #{@target.class}")
begin
result = @target.public_send(method_name, *args, **kwargs, &block)
duration = Time.now - start_time
@logger.info("#{method_name} completed in #{duration}s")
result
rescue => error
@logger.error("#{method_name} failed: #{error.message}")
raise
end
end
def respond_to_missing?(method_name, include_private = false)
@target.respond_to?(method_name, include_private) || super
end
end
class DatabaseConnection
def query(sql)
sleep(0.1) # Simulate database query
"Result for: #{sql}"
end
def transaction
yield if block_given?
end
end
db = DatabaseConnection.new
proxy = LoggingProxy.new(db)
result = proxy.query("SELECT * FROM users")
# I, [timestamp] INFO -- : Calling query on DatabaseConnection
# I, [timestamp] INFO -- : query completed in 0.1s
Module integration patterns extend method_missing
functionality across class hierarchies. Use included
and extended
hooks to inject method_missing
behavior into multiple classes while maintaining clean separation of concerns.
module DynamicAttributes
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def dynamic_attribute(name, type: :string, default: nil)
@dynamic_attributes ||= {}
@dynamic_attributes[name.to_s] = { type: type, default: default }
end
def dynamic_attributes
@dynamic_attributes || {}
end
end
def initialize(*args)
super
@dynamic_values = {}
# Set defaults for dynamic attributes
self.class.dynamic_attributes.each do |name, config|
@dynamic_values[name] = config[:default]
end
end
def method_missing(method_name, *args)
name = method_name.to_s
dynamic_attrs = self.class.dynamic_attributes
if name.end_with?('=')
attr_name = name.chomp('=')
if dynamic_attrs.key?(attr_name)
@dynamic_values[attr_name] = cast_value(args.first, dynamic_attrs[attr_name][:type])
return args.first
end
elsif args.empty? && dynamic_attrs.key?(name)
return @dynamic_values[name]
end
super
end
def respond_to_missing?(method_name, include_private = false)
name = method_name.to_s
dynamic_attrs = self.class.dynamic_attributes
(name.end_with?('=') && dynamic_attrs.key?(name.chomp('='))) ||
dynamic_attrs.key?(name) ||
super
end
private
def cast_value(value, type)
case type
when :integer then value.to_i
when :float then value.to_f
when :boolean then !!value
else value.to_s
end
end
end
class Product
include DynamicAttributes
dynamic_attribute :sku, type: :string
dynamic_attribute :price, type: :float, default: 0.0
dynamic_attribute :in_stock, type: :boolean, default: true
def initialize(name)
@name = name
super()
end
end
product = Product.new("Widget")
product.sku = "WIDGET-001"
product.price = 29.99
product.in_stock = false
puts product.sku # => "WIDGET-001"
puts product.price # => 29.99
puts product.in_stock # => false
Error Handling & Debugging
Debugging method_missing
implementations requires understanding Ruby's method lookup chain and common failure modes. The most frequent issues involve infinite recursion, missing respond_to_missing?
definitions, and incorrect super
calls.
Infinite recursion occurs when method_missing
implementations inadvertently call undefined methods on themselves. This creates a loop where each undefined method call triggers another method_missing
call.
class ProblematicHandler
def method_missing(method_name, *args)
# BUG: If @logger is nil, this creates infinite recursion
@logger.info("Missing method: #{method_name}")
super
end
end
# Safe implementation with explicit checks
class SafeHandler
def method_missing(method_name, *args)
# Check if logger exists before using
if defined?(@logger) && @logger
@logger.info("Missing method: #{method_name}")
end
super
end
end
Debugging method_missing
calls requires careful inspection of the method lookup process. Use Ruby's introspection methods and debugging techniques to trace method resolution failures.
class DebuggingProxy
def initialize(target)
@target = target
end
def method_missing(method_name, *args, **kwargs, &block)
puts "DEBUG: method_missing called for #{method_name}"
puts "DEBUG: Arguments: #{args.inspect}"
puts "DEBUG: Keyword args: #{kwargs.inspect}"
puts "DEBUG: Block given: #{block_given?}"
# Check if target can handle the method
if @target.respond_to?(method_name)
puts "DEBUG: Forwarding to target"
@target.public_send(method_name, *args, **kwargs, &block)
else
puts "DEBUG: Target cannot handle #{method_name}"
puts "DEBUG: Target methods: #{@target.methods(false).sort}"
super
end
rescue => error
puts "DEBUG: Error in method_missing: #{error.class}: #{error.message}"
puts "DEBUG: Backtrace:"
error.backtrace.first(5).each { |line| puts " #{line}" }
raise
end
end
Exception handling within method_missing
must account for errors from both the dynamic method implementation and the original method call. Distinguish between expected missing methods and unexpected errors in method dispatch.
class RobustHandler
def initialize
@data = {}
end
def method_missing(method_name, *args)
name = method_name.to_s
begin
if name.end_with?('=')
handle_setter(name.chomp('='), args.first)
elsif args.empty?
handle_getter(name)
else
# Don't handle methods with arguments that aren't setters
raise NoMethodError, "undefined method `#{method_name}' for #{self}:#{self.class}"
end
rescue ArgumentError => e
# Re-raise as NoMethodError to maintain interface expectations
raise NoMethodError, "#{method_name}: #{e.message}"
rescue => e
# Log unexpected errors but preserve the original exception
warn "Unexpected error in method_missing for #{method_name}: #{e.message}"
raise
end
end
private
def handle_setter(name, value)
validate_attribute_name!(name)
@data[name] = value
end
def handle_getter(name)
validate_attribute_name!(name)
@data.fetch(name) { raise NoMethodError, "undefined method `#{name}'" }
end
def validate_attribute_name!(name)
raise ArgumentError, "Invalid attribute name: #{name}" unless name.match?(/\A[a-zA-Z_]\w*\z/)
end
end
Testing method_missing
implementations requires strategies for both positive and negative test cases. Verify that intended dynamic methods work correctly and that unhandled methods still raise appropriate errors.
require 'minitest/autorun'
class TestDynamicHandler < Minitest::Test
def setup
@handler = DynamicHandler.new
end
def test_dynamic_setter_and_getter
@handler.name = "test"
assert_equal "test", @handler.name
end
def test_respond_to_missing_positive
@handler.name = "test"
assert @handler.respond_to?(:name)
assert @handler.respond_to?(:name=)
end
def test_respond_to_missing_negative
refute @handler.respond_to?(:undefined_method)
end
def test_undefined_method_raises_error
assert_raises(NoMethodError) do
@handler.some_undefined_method_with_args(1, 2, 3)
end
end
def test_error_message_format
error = assert_raises(NoMethodError) do
@handler.undefined_method
end
assert_match(/undefined method/, error.message)
assert_match(/DynamicHandler/, error.message)
end
end
Common Pitfalls
Method caching issues represent the most subtle pitfall in method_missing
implementations. Ruby's method cache can interfere with dynamic method behavior, causing inconsistent results when methods are defined at runtime.
class CachingProblem
def method_missing(method_name, *args)
# Define the method dynamically for future calls
self.class.define_method(method_name) do |*method_args|
"Dynamic #{method_name} with #{method_args.inspect}"
end
# Call the newly defined method
public_send(method_name, *args)
end
end
obj = CachingProblem.new
obj.test_method(1, 2) # Works fine
obj2 = CachingProblem.new
obj2.test_method(3, 4) # Already defined, no method_missing called
The respond_to_missing?
omission causes object introspection to fail, breaking Duck Typing and metaprogramming that relies on respond_to?
checks. This violates the Liskov Substitution Principle and can cause subtle bugs in frameworks.
# Broken: Missing respond_to_missing?
class BrokenDynamic
def method_missing(method_name, *args)
"handled #{method_name}"
end
end
# Fixed: Proper respond_to_missing? implementation
class FixedDynamic
def method_missing(method_name, *args)
"handled #{method_name}"
end
def respond_to_missing?(method_name, include_private = false)
true # or more specific logic
end
end
broken = BrokenDynamic.new
fixed = FixedDynamic.new
broken.respond_to?(:anything) # => false (wrong!)
broken.anything # => "handled anything" (works)
fixed.respond_to?(:anything) # => true (correct!)
fixed.anything # => "handled anything" (works)
Performance degradation occurs because method_missing
bypasses Ruby's optimized method cache. Every call to a missing method triggers the full method lookup chain plus the method_missing
callback, making dynamic methods significantly slower than defined methods.
class PerformanceComparison
def defined_method
"defined"
end
def method_missing(method_name, *args)
"missing"
end
end
require 'benchmark'
obj = PerformanceComparison.new
Benchmark.bm(20) do |x|
x.report("defined method:") do
100_000.times { obj.defined_method }
end
x.report("method_missing:") do
100_000.times { obj.missing_method }
end
end
# Results show method_missing is ~10x slower
# user system total real
# defined method: 0.010000 0.000000 0.010000 ( 0.008234)
# method_missing: 0.090000 0.000000 0.090000 ( 0.089567)
Inheritance complications arise when multiple classes in a hierarchy implement method_missing
. The method resolution order determines which implementation runs, potentially masking intended behavior in subclasses.
class Parent
def method_missing(method_name, *args)
"Parent handled #{method_name}"
end
end
class Child < Parent
def method_missing(method_name, *args)
if method_name.to_s.start_with?('child_')
"Child handled #{method_name}"
else
super # Important: call parent's method_missing
end
end
end
class ProblemChild < Parent
def method_missing(method_name, *args)
# BUG: Never calls super, Parent's logic never runs
"ProblemChild handled #{method_name}"
end
end
child = Child.new
child.child_method # => "Child handled child_method"
child.parent_method # => "Parent handled parent_method"
problem = ProblemChild.new
problem.anything # => "ProblemChild handled anything"
# Parent's implementation never reached
Thread safety issues emerge when method_missing
implementations modify shared state without proper synchronization. Multiple threads calling missing methods simultaneously can corrupt instance variables or class-level data structures.
class ThreadUnsafe
def initialize
@cache = {}
end
def method_missing(method_name, *args)
# BUG: Race condition on @cache access
@cache[method_name] ||= compute_value(method_name, args)
end
private
def compute_value(name, args)
# Simulate expensive computation
sleep(0.01)
"computed #{name}"
end
end
# Thread-safe version
class ThreadSafe
def initialize
@cache = {}
@mutex = Mutex.new
end
def method_missing(method_name, *args)
@mutex.synchronize do
@cache[method_name] ||= compute_value(method_name, args)
end
end
private
def compute_value(name, args)
sleep(0.01)
"computed #{name}"
end
end
Reference
BasicObject Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#method_missing(symbol, *args, **kwargs, &block) |
Method name (Symbol), arguments (Array), keyword arguments (Hash), block (Proc) | Object |
Called when undefined method is invoked |
#respond_to_missing?(symbol, include_private) |
Method name (Symbol), include private flag (Boolean) | Boolean |
Returns true if method_missing handles the method |
Method Missing Parameters
Parameter | Type | Description |
---|---|---|
method_name |
Symbol |
Name of the undefined method being called |
*args |
Array |
Positional arguments passed to the undefined method |
**kwargs |
Hash |
Keyword arguments passed to the undefined method |
&block |
Proc |
Block passed to the undefined method |
Common Implementation Patterns
Pattern | Use Case | Performance | Complexity |
---|---|---|---|
Attribute accessor | Dynamic getters/setters | Medium | Low |
Method delegation | Proxy objects | Low | Medium |
DSL creation | Domain-specific languages | Low | High |
Method caching | One-time method definition | High after first call | Medium |
Error Handling Guidelines
Scenario | Action | Reason |
---|---|---|
Unhandled method | Call super |
Maintains Ruby's error semantics |
Invalid arguments | Raise ArgumentError |
Clear error messaging |
Internal errors | Let exceptions propagate | Preserves debugging information |
Expected missing methods | Return appropriate value | Implements intended dynamic behavior |
Performance Characteristics
Operation | Relative Speed | Notes |
---|---|---|
Defined method call | 1x (baseline) | Uses Ruby's method cache |
method_missing call |
10-20x slower | Full method lookup + callback |
Cached dynamic method | 1x after definition | Same as defined method |
Delegation via method_missing |
15-25x slower | Additional method dispatch overhead |
Thread Safety Considerations
Implementation Type | Thread Safety | Required Protection |
---|---|---|
Read-only behavior | Safe | None |
Instance variable modification | Unsafe | Mutex or similar synchronization |
Class variable modification | Unsafe | Class-level synchronization |
Method definition at runtime | Partially safe | Consider method cache invalidation |
Integration with Ruby Features
Feature | Compatibility | Notes |
---|---|---|
Module inclusion | Full | method_missing follows inheritance chain |
Singleton methods | Full | Checked before method_missing |
public_send /send |
Full | Calls method_missing for undefined methods |
respond_to? |
Requires respond_to_missing? |
Must implement both methods |
Method objects | Not supported | Cannot create Method objects for missing methods |