CrackedRuby logo

CrackedRuby

method_missing

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