35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Ruby method_missing: When to Use It and When to Run Away

Ruby method_missing: When to Use It and When to Run Away

TTB Software
ruby

When you call a method that doesn’t exist on a Ruby object, Ruby doesn’t just crash. It calls method_missing on that object, passing the method name and arguments. This hook lets you intercept undefined method calls and handle them however you want.

class FlexibleConfig
  def initialize(data)
    @data = data
  end

  def method_missing(name, *args)
    key = name.to_s
    if @data.key?(key)
      @data[key]
    else
      super
    end
  end

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

config = FlexibleConfig.new("host" => "localhost", "port" => 5432)
config.host  # => "localhost"
config.port  # => 5432
config.nope  # => NoMethodError

That super call on line 10 is critical. Without it, you swallow every missing method silently, and debugging becomes a nightmare.

Why respond_to_missing? Is Not Optional

Every method_missing implementation needs a matching respond_to_missing?. This isn’t a suggestion — it’s a contract. Without it, your object lies about its capabilities:

config = FlexibleConfig.new("host" => "localhost")

# Without respond_to_missing?:
config.host           # => "localhost" (works)
config.respond_to?(:host)  # => false (broken!)
method(:host)              # => NameError (broken!)

# With respond_to_missing?:
config.respond_to?(:host)  # => true (correct)
config.method(:host)       # => #<Method: ...> (correct)

Rails itself got this wrong in early versions. The respond_to_missing? method was added in Ruby 1.9.2 specifically because respond_to? and method_missing being out of sync caused real bugs across major libraries. See Ruby core issue #3008 for the original discussion.

The rule: if you override method_missing, you override respond_to_missing? with the same logic. Always.

Real-World Uses That Make Sense

Proxy Objects

The strongest use case for method_missing is building proxy objects that delegate to something else. Ruby’s Delegator and BasicObject classes exist partly for this pattern:

class TimedProxy
  def initialize(target, logger:)
    @target = target
    @logger = logger
  end

  def method_missing(name, *args, **kwargs, &block)
    if @target.respond_to?(name)
      start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
      result = @target.public_send(name, *args, **kwargs, &block)
      elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
      @logger.debug("#{@target.class}##{name} took #{(elapsed * 1000).round(1)}ms")
      result
    else
      super
    end
  end

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

ActiveRecord’s association proxies use this pattern internally. When you call user.posts.published, the association proxy intercepts published through method_missing and forwards it to the scope.

Dynamic Finders (The Classic Example)

Before Rails 4 deprecated most dynamic finders, find_by_email, find_by_name_and_age, and similar methods all worked through method_missing:

# Simplified version of how Rails 3 did it
def method_missing(name, *args)
  if name.to_s =~ /^find_by_(.+)$/
    columns = $1.split("_and_")
    where(columns.zip(args).to_h).first
  else
    super
  end
end

Rails moved away from this toward explicit find_by(email: ...) calls. That shift is instructive — even the Rails team decided the tradeoff wasn’t worth it for most cases.

Builder/DSL Patterns

method_missing works well for XML/HTML builders and DSL construction:

class HtmlBuilder
  def initialize
    @html = +""
  end

  def method_missing(tag, content = nil, **attrs, &block)
    attr_str = attrs.map { |k, v| %( #{k}="#{v}") }.join
    @html << "<#{tag}#{attr_str}>"
    if block
      nested = self.class.new
      nested.instance_eval(&block)
      @html << nested.to_s
    elsif content
      @html << content.to_s
    end
    @html << "</#{tag}>"
    self
  end

  def respond_to_missing?(*, **)
    true  # Any tag name is valid
  end

  def to_s
    @html
  end
end

Nokogiri’s Builder and Markaby both use variations of this approach.

The Performance Cost

method_missing is slower than regular method dispatch. Here’s a benchmark on Ruby 3.3.6 with YJIT enabled:

require "benchmark/ips"

class Direct
  def greet = "hello"
end

class Dynamic
  def method_missing(name, *)
    "hello" if name == :greet
  end
  def respond_to_missing?(name, *) = name == :greet
end

Benchmark.ips do |x|
  direct = Direct.new
  dynamic = Dynamic.new

  x.report("direct")  { direct.greet }
  x.report("method_missing") { dynamic.greet }
  x.compare!
end

Results on an M2 MacBook Pro:

direct:           42.3M i/s
method_missing:    8.7M i/s - 4.86x slower

That’s roughly 5x slower per call. For a configuration object accessed once at boot, irrelevant. For something called in a tight loop processing thousands of records, it adds up fast.

YJIT can’t optimize method_missing the way it optimizes regular method calls, because the target method isn’t known at compile time. This gap will likely persist across Ruby versions.

The define_method Escape Hatch

If you need method_missing for discovery but want fast repeated access, define the method on first call:

class CachedConfig
  def initialize(data)
    @data = data
  end

  def method_missing(name, *args)
    key = name.to_s
    if @data.key?(key)
      # Define the method so next call is fast
      self.class.define_method(name) { @data[key] }
      @data[key]
    else
      super
    end
  end

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

First call goes through method_missing. Every subsequent call hits the defined method at full speed. ActiveRecord uses this technique for attribute accessors — the first access to user.name defines a real method, so the second access is a normal method call.

When to Use Something Else

Most times you reach for method_missing, a simpler tool exists:

Situation Instead of method_missing, use
Delegating to another object Forwardable or delegate in Rails
Dynamic attribute access define_method at class load time
Config/settings object Struct, Data, or OpenStruct
A few known dynamic methods Define them explicitly with define_method

OpenStruct itself uses method_missing under the hood, but it handles all the edge cases for you. In Ruby 3.0+, OpenStruct also triggers a deprecation warning when used in certain contexts because of the performance overhead — which tells you something about the Ruby core team’s current thinking on this pattern.

If you know all possible method names at class definition time, use define_method in a loop:

class Config
  FIELDS = %w[host port database username password].freeze

  FIELDS.each do |field|
    define_method(field) { @data[field] }
  end

  def initialize(data)
    @data = data
  end
end

No method_missing needed. Full YJIT optimization. Proper respond_to? behavior for free. Methods show up in instance_methods. Debugging is straightforward.

Debugging method_missing Gone Wrong

The worst bug pattern with method_missing is the infinite loop. It happens when method_missing accidentally calls another missing method:

# DO NOT DO THIS
def method_missing(name, *args)
  if name.to_s.start_with?("find_")
    # Oops: 'log' is also undefined, triggering method_missing again
    log("Looking up #{name}")
    # ...
  end
end

Ruby’s default stack depth limit (usually around 10,000 frames) will eventually catch this with a SystemStackError, but the backtrace will be incomprehensible.

Debugging tip: When you hit a SystemStackError or a NoMethodError that makes no sense, add a temporary debug line at the top of method_missing:

def method_missing(name, *args)
  $stderr.puts "method_missing called: #{name} on #{self.class}"
  # ... rest of implementation
end

This immediately reveals unexpected calls.

Production Checklist

Before shipping code that uses method_missing:

  1. respond_to_missing? is implemented with matching logic
  2. super is called for unhandled method names
  3. Performance is acceptable for the call frequency
  4. Consider define_method caching for repeated calls
  5. Tests cover the respond_to? contract, not just the method calls
  6. You actually need it — if the set of methods is known, use define_method instead

FAQ

Does method_missing work with keyword arguments in Ruby 3.3?

Yes, but you need to explicitly handle them. Since Ruby 3.0 separated positional and keyword arguments, your method_missing signature should include **kwargs:

def method_missing(name, *args, **kwargs, &block)

If you omit **kwargs, any keyword arguments passed to a missing method will raise an ArgumentError before method_missing even runs.

Can method_missing intercept private method calls?

No. method_missing only fires for public method calls by default. If you call a private method from outside the object, Ruby raises NoMethodError with a “private method called” message — method_missing is not invoked. Inside the object, private methods resolve normally and never reach method_missing.

How do gems like ActiveRecord use method_missing safely?

ActiveRecord pairs method_missing with the define_method caching pattern described above. The first attribute access goes through method_missing, which then defines a real method on the class. Subsequent accesses hit the defined method directly. This gives dynamic behavior at load time with full performance afterward. You can see this in ActiveModel::AttributeMethods#method_missing in the Rails source.

Is method_missing slower with YJIT enabled?

YJIT improves regular method dispatch significantly (20-30% in typical Rails apps), but it can’t optimize method_missing calls because the target is unknown at compile time. The relative performance gap between regular methods and method_missing actually widens with YJIT, because regular methods get faster while method_missing stays roughly the same. In our benchmarks, the gap went from ~3x (no YJIT) to ~5x (YJIT enabled).

Should I use BasicObject instead of Object for proxy classes?

BasicObject strips out almost all methods (no to_s, no ==, no inspect), so more calls fall through to method_missing. This is useful for pure proxy objects where you want maximum transparency. But it also means basic operations like string interpolation and equality checks break unless you handle them. Use BasicObject only when you need a truly transparent proxy and you’re prepared to handle the edge cases.

#ruby #metaprogramming #method_missing #respond_to_missing #design-patterns
T

About the Author

Roger Heykoop is a senior Ruby on Rails developer with 19+ years of Rails experience and 35+ years in software development. He specializes in Rails modernization, performance optimization, and AI-assisted development.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation