/ Tags: RUBY 3 / Categories: RUBY

method_missing and respond_to_missing? — Ruby Metaprogramming That's Actually Useful

Metaprogramming has a reputation for being either magical or dangerous, depending on who you ask. Both reactions are earned. Ruby’s method_missing hook lets you intercept calls to methods that don’t exist and handle them dynamically — which is powerful when used carefully and a debugging nightmare when abused. The trick is knowing what it’s actually good for, implementing it correctly alongside respond_to_missing?, and resisting the urge to reach for it when a plain method would do.

How method_missing Works


When you call a method on an object and Ruby can’t find it in the method lookup chain — the object’s class, its ancestors, its mixins — it calls method_missing as a last resort. By default, method_missing raises NoMethodError. Override it, and you can intercept that call and do something useful.

Example:

class FlexibleLogger
  def method_missing(name, *args, **kwargs, &block)
    if name.to_s.start_with?("log_")
      level = name.to_s.sub("log_", "")
      puts "[#{level.upcase}] #{args.first}"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s.start_with?("log_") || super
  end
end

logger = FlexibleLogger.new
logger.log_info("Server started")     # => [INFO] Server started
logger.log_warning("Disk space low")  # => [WARNING] Disk space low
logger.log_error("DB connection lost") # => [ERROR] DB connection lost

method_missing catches log_info, log_warning, and log_error — none of which are defined explicitly. The pattern check routes to the logging logic; anything else calls super, which propagates the NoMethodError normally.

The Critical Partner: respond_to_missing?


Here’s where most method_missing implementations go wrong: they override method_missing but forget respond_to_missing?. The result is an object that handles dynamic method calls but lies about its capabilities.

Example:

logger = FlexibleLogger.new

logger.respond_to?(:log_info)   # => false (wrong — it DOES respond)
logger.method(:log_info)        # => NameError

respond_to? checks respond_to_missing? for methods not defined normally. If you don’t override it, Ruby assumes those methods don’t exist. Code that checks respond_to? before calling (a common defensive pattern) breaks silently.

The fix is always to pair method_missing with respond_to_missing?:

Example:

def respond_to_missing?(name, include_private = false)
  name.to_s.start_with?("log_") || super
end

logger.respond_to?(:log_info)  # => true
logger.method(:log_info)       # works

Rule: If you implement method_missing, always implement respond_to_missing? with matching logic.

A More Real-World Pattern: Dynamic Finders


ActiveRecord uses method_missing internally to power find_by_name, find_by_email, and similar dynamic finders (in older Rails versions). Here’s a simplified version that illustrates the pattern:

Example:

class UserRepository
  USERS = [
    { id: 1, name: "Alice", role: "admin" },
    { id: 2, name: "Bob",   role: "user"  },
    { id: 3, name: "Carol", role: "admin" }
  ]

  def method_missing(name, *args, **kwargs, &block)
    match = name.to_s.match(/\Afind_by_(\w+)\z/)
    if match
      attribute = match[1].to_sym
      USERS.find { |u| u[attribute] == args.first }
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s.match?(/\Afind_by_\w+\z/) || super
  end
end

repo = UserRepository.new
repo.find_by_name("Alice")  # => { id: 1, name: "Alice", role: "admin" }
repo.find_by_role("admin")  # => { id: 1, name: "Alice", role: "admin" }

One method_missing implementation handles find_by_anything dynamically. Adding a new attribute to search by requires zero code changes.

define_method as the Better Alternative


For many use cases where method_missing feels tempting, define_method is cleaner. Instead of intercepting every unknown call, you generate the methods explicitly at class definition time:

Example:

class FlexibleLogger
  %w[info warning error debug].each do |level|
    define_method("log_#{level}") do |message|
      puts "[#{level.upcase}] #{message}"
    end
  end
end

logger = FlexibleLogger.new
logger.log_info("Server started")
logger.respond_to?(:log_info)   # => true (no method_missing needed)
logger.method(:log_info)        # => works normally

define_method creates real methods at load time. They show up in respond_to?, method, and introspection tools. They appear in backtraces. They’re faster at runtime — no dynamic dispatch overhead.

Use define_method when the set of methods is known at class definition time. Use method_missing when the method names are truly dynamic and unknowable in advance.

Pro-Tip: method_missing is expensive. Every call that hits it goes through Ruby’s full method lookup chain first. If you’re calling dynamic methods in a tight loop, the overhead adds up. Profile before assuming it’s fast enough. And in any method_missing implementation, always call super in the else branch — not raising NoMethodError yourself — so that other method_missing implementations in the ancestor chain still work correctly.

When Not to Use method_missing


The temptation is to reach for method_missing anytime you want “magic” behavior. Resist it. The debugging experience is poor — stack traces are harder to read, tools like binding.pry and language servers can’t autocomplete or navigate to dynamic methods, and the failure mode when something goes wrong is often confusing.

Good uses: DSLs that accept arbitrary method names (like RSpec matchers), proxy objects that forward calls to another object, adapters that map methods to external APIs with varying names.

Bad uses: avoiding writing repetitive but straightforward methods, “just to be clever,” anything where a hash lookup or define_method would work just as well.

Conclusion


method_missing is one of Ruby’s genuinely powerful features — and one of its most misused. Used correctly, with respond_to_missing? always alongside it and super always in the fallback, it enables DSLs and adapters that would otherwise require significant boilerplate. Used carelessly, it creates objects that lie about their interface and code that’s hard to debug. Know the tool, know the alternative (define_method), and reach for whichever one makes the code clearer.

FAQs


Q1: Does method_missing work with keyword arguments in Ruby 3?
Yes. Include **kwargs in the signature: def method_missing(name, *args, **kwargs, &block). In Ruby 3, positional and keyword arguments are strictly separated, so the double-splat is necessary to correctly capture keyword arguments passed to the missing method.

Q2: Can method_missing slow down my application?
Yes, measurably in tight loops. Ruby traverses the entire ancestor chain before calling method_missing. For rarely-called code paths it’s negligible; for hot paths, benchmark and consider define_method instead.

Q3: How do I see which methods method_missing is intercepting?
Add a puts or Rails.logger.debug inside your method_missing with the name argument during development. Or use set_trace_func / TracePoint to trace method calls. The name argument is always the Symbol of the missing method name.

Q4: What’s the difference between method_missing and BasicObject#method_missing?
BasicObject is Ruby’s root object with almost no methods. BasicObject#method_missing is the ultimate fallback — it raises NoMethodError. When you call super from your method_missing, the call eventually reaches this. Blank slate objects (proxy objects that forward everything) sometimes inherit from BasicObject to avoid inheriting Object’s methods.

Q5: Can I use method_missing in a module that’s included into other classes?
Yes. Modules can define method_missing and respond_to_missing?. When included, these hook into the host class’s method lookup. Be careful about specificity in your pattern matching — a module’s method_missing shouldn’t swallow calls meant for the host class or other mixins.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More