skip to content

In Ruby, how do you write method_missing for a settings object that answers any config key as a method, and why must it call super?

level: middleimportance: must knowfreq 65%

answer

  1. match only names you handle
  2. check argument count too
  3. bare super for the rest
  4. typos must still raise
  5. stack level too deep

basics

~20 s

Override method_missing to return the stored value when the name is a known key, handle key= writers, and call bare super for every other name. Without super, typos return nil silently instead of raising NoMethodError.

solid answer

~40 s

I keep the values in a Hash and override `method_missing(name, *args, &block)`: if `name` is a known key and no arguments were given, return the value; if it ends in `=` with one argument and names an existing key, store it; otherwise call bare `super`, which forwards the same name, arguments and block to the default handler. That `super` matters: without it an unknown name like `settings.prot` returns `nil` and the bug surfaces far away, whereas `super` raises the usual `NoMethodError` with the caller's line. I pair it with `respond_to_missing?`, avoid calling any undefined method inside it, since that recurses until `SystemStackError`, and reject keys such as `display` or `freeze` that real methods would shadow.

code

ruby · 11 lines
ruby
class Settings
  def initialize(values) = @values = values

  def method_missing(name, *args)
    @values.fetch(nme) { super }   # typo: nme
  end
end

Settings.new({}).port
# nme is itself a missing name, so method_missing calls itself
# until SystemStackError: stack level too deep

go deeper

for a junior

Recall the shape: match the names you handle, answer them, call super for the rest.

for a middle

Explain what bare super forwards, why skipping it hides typos, and how argument checks separate readers from writers.

for a senior

Show production judgement: the recursion trap, keys shadowed by real methods, and choosing Struct, Data or fetch when names are not open-ended.

for a principal

Decide whether ghost-method convenience is worth the lost discoverability for shared configuration objects across a codebase.

## The goal A settings object loaded from a YAML or JSON file holds keys such as `port`, `host` and `timeout`. Callers would rather write `settings.port` than `settings["port"]`. The keys are not known when the class is written, so there is no method to define in advance; `method_missing` can answer them. ## A correct implementation ```ruby class Settings def initialize(values) = @values = values.transform_keys(&:to_s) def method_missing(name, *args, &block) key = name.to_s if key.end_with?("=") && args.size == 1 && @values.key?(key.chomp("=")) @values[key.chomp("=")] = args.first elsif @values.key?(key) && args.empty? @values[key] else super end end def respond_to_missing?(name, include_private = false) key = name.to_s.chomp("=") @values.key?(key) || super end end settings = Settings.new(port: 8080, host: "db") settings.port # => 8080 settings.port = 9090 settings.prot # NoMethodError: undefined method 'prot' for an instance of Settings ``` The shape to remember: 1. **Recognise** only names you really handle, and check the arguments too: a reader takes none, a writer exactly one and only for an existing key. 2. **Answer** those names. 3. **Call `super`** for everything else, with no arguments, so the original name, arguments and block go to the default handler. 4. **Pair it with `respond_to_missing?`**, so `respond_to?` and `method` agree with what `method_missing` answers. ## Why super is not optional Without `super`, the last branch would return `nil` or some other value for every unknown name. A typo such as `settings.prot` would then quietly return `nil`, and the failure would surface later and far away, as a `nil` port passed to a socket. Calling `super`: - raises the normal `NoMethodError` with the real name and the caller's line in the backtrace; - keeps any ancestor's `method_missing` working, for example one added by a module included in `Settings`; - preserves the **error class**: a bare identifier inside the class still gives `NameError`, because the default handler knows how the original call was written. Bare `super` passes along exactly what `method_missing` received. If you narrow the arguments by hand, use `super(name, *args, &block)` rather than dropping some. ## Traps specific to method_missing | Trap | What happens | Fix | |---|---|---| | Calling an undefined method inside `method_missing` | it calls `method_missing` again, forever, until `SystemStackError` ("stack level too deep") | use only defined methods and instance variables inside it | | A key named like a public method (`display`, `freeze`, `hash`) | the real method runs; `method_missing` is never reached | reject such keys at load time, or read with `[]` | | A key named like a private `Kernel` method (`format`, `system`) | reaches `method_missing`, because a private method called with a receiver counts as missing | same: validate keys or use `[]` | | Defining `method_missing` on `Object` itself | Ruby warns "redefining Object#method_missing may cause infinite loop" and every object is affected | define it in the one class that needs it | ## Testing it A small set of tests pins the behaviour down: - a known key returns its value, and a writer stores a new one; - an unknown name raises `NoMethodError` with that name (`error.name == :prot`); - `respond_to?` agrees with `method_missing` for a known key, a writer and an unknown name; - a reader called with an argument is refused, so `settings.port(1)` does not silently ignore it. ## When not to use it For a fixed set of fields, `Struct` or `Data.define` gives real methods, real `respond_to?` answers and better speed. `method_missing` is for names that truly are open-ended, like keys in a loaded file. Even then, a plain `fetch("port")` API is simpler; ghost methods are a convenience that must be earned. ## What an interviewer is listening for The expected answer shows a `method_missing` that matches only the names it handles, returns their values, and calls `super` otherwise, explaining that skipping `super` turns typos into silent `nil`s. Strong answers mention `respond_to_missing?`, argument checks for readers and writers, the recursion trap and keys that collide with real methods.

  • What is the difference between bare super and super(name) inside method_missing?
    Bare `super` forwards exactly what `method_missing` received: the name, every argument and the block. `super(name)` sends only the name, so the default error loses the arguments (`NoMethodError#args` is empty) and an ancestor's handler that needs them gets nothing. Use bare `super`, or pass all of `name, *args, &block` explicitly.
  • Why does settings.display print the object instead of returning the display key?
    `display` is a public `Kernel` method, so lookup finds it and `method_missing` is never called. Ghost methods only answer names that no ancestor defines publicly. Keys that collide with real methods must be rejected when loading, renamed, or read through `[]`.

method_missing is a receptionist covering an empty desk: she answers questions she can, and passes every other caller to the switchboard instead of inventing an answer.

saying these in an interview costs you the question

  • returning nil for unknown names is fine, callers can check
  • super inside method_missing is only needed with inheritance
  • every key, including display or freeze, reaches method_missing
  • method_missing can safely call helper methods it has not defined
  • super(name) forwards the arguments and block as well