Ruby method_missing: When to Use It and When to Run Away
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:
respond_to_missing?is implemented with matching logicsuperis called for unhandled method names- Performance is acceptable for the call frequency
- Consider
define_methodcaching for repeated calls - Tests cover the
respond_to?contract, not just the method calls - You actually need it — if the set of methods is known, use
define_methodinstead
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.
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 TouchRelated Articles
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