Skip to Content
Course content

148: Method Missing vs respond_to_missing?

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably encountered "ghost methods" before—methods that aren't actually defined in the class but somehow still work when you call them. In Ruby, this magic happens via method_missing. It's a powerful tool, but it comes with a responsibility. If you tell Ruby how to handle a missing method, you also have to tell Ruby that the object actually responds to that method.

Creating a flexible settings object

Let's build a AppConfig class. I want to be able to pass a hash of settings into it and then access those settings as if they were methods. It's much cleaner to write config.api_key than config[:api_key] throughout a large codebase.

class AppConfig
  def initialize(settings = {})
    @settings = settings
  end

  def method_missing(method_name, *args, &block)
    if @settings.key?(method_name)
      @settings[method_name]
    else
      super
    end
  end
end

config = AppConfig.new(api_key: "secret_123", timeout: 30)
puts config.api_key # => "secret_123"
puts config.timeout # => 30

This works great for the happy path. I'm intercepting the call, checking my internal hash, and returning the value. If the key doesn't exist, I call super, which eventually triggers a NoMethodError, just as it should.

The lie we're telling the rest of the app

Here is where I usually trip up when I'm rushing a feature. I've implemented the logic, but I've forgotten that Ruby's introspection tools don't know about my method_missing logic. Watch what happens when we try to check if the object can handle a specific call:

config = AppConfig.new(api_key: "secret_123")
puts config.respond_to?(:api_key) # => false

Wait, what? I just called config.api_key and it worked perfectly! But respond_to? is lying to me. This is a problem because many gems and libraries use respond_to? to decide whether or not to call a method. If you're using a library that checks for capabilities before executing them, your AppConfig object will be seen as useless, even though it functions fine.

Aligning the truth with respond_to_missing?

To fix this, we use respond_to_missing?. This is a special hook that Ruby calls whenever respond_to? is invoked. Instead of overriding respond_to? directly (which is dangerous and old-school), we override respond_to_missing? to define the "truth" about our ghost methods.

class AppConfig
  def initialize(settings = {})
    @settings = settings
  end

  def method_missing(method_name, *args, &block)
    if @settings.key?(method_name)
      @settings[method_name]
    else
      super
    end
  end

  def respond_to_missing?(method_name, include_private = false)
    @settings.key?(method_name) || super
  end
end

config = AppConfig.new(api_key: "secret_123")
puts config.respond_to?(:api_key) # => true
puts config.respond_to?(:unknown) # => false

Notice that I added || super at the end of the respond_to_missing? method. This is crucial. Your class might inherit from another class that also uses method_missing, or it might have other inherited behaviors. By calling super, you ensure that you aren't accidentally silencing other parts of the Ruby object hierarchy.

The rule of thumb I follow is simple: If you find yourself typing def method_missing, your very next line of code should be def respond_to_missing?. They are two sides of the same coin.




📋 Practical Task

Building a Dynamic Translation Proxy

You are building a translation system where you want to call methods like t.welcome_message or t.error_not_found, and have the class look those keys up in a translation dictionary.

Create a class called Translator that:

  • Takes a hash of translations in the initialize method.
  • Implements method_missing so that calling a method returns the corresponding value from the hash.
  • Correctly implements respond_to_missing? so that respond_to? returns true for keys that exist in the dictionary and false for those that don't.
Test your implementation with the following:
translator = Translator.new(welcome: "Hello!", goodbye: "Bye!")
puts translator.welcome     # Should print "Hello!"
puts translator.respond_to?(:welcome)  # Should print true
puts translator.respond_to?(:missing)  # Should print false
Rating
0 0

There are no comments for now.

to be the first to leave a comment.