In Ruby 4.0, a product-title chain fails with `undefined method 'capitalize' for nil`; how do you pinpoint which link returned nil and fix it?
answer
- the culprit is the link before
- error_highlight underlines the call
- NameError#receiver and #name
- tap to print intermediates
- self or nil, [] on a miss, find
basics
~20 sThe method named in the NoMethodError, capitalize, is the link after the culprit; the link before it returned nil. error_highlight's caret marks which call failed, and a tap inserted between links shows intermediate values. Fix the link that can return nil.
solid answer
~40 sThe message names the method that was **called on** `nil`, so the bug is the link immediately before `.capitalize`. On the reported line, Ruby's `error_highlight` prints the source with `^^^^` under the exact failing call, which matters when a line holds several. Then ask which methods in the chain *can* return `nil`: bang methods documented as `self or nil` (`strip!`, `squeeze!`), `Hash#[]` on a missing key, `Array#first` or `find` on no match, `String#[]` with an unmatched pattern. To confirm, insert `.tap { |v| warn v.inspect }` between links, or split the chain into named locals. In a `rescue`, the exception's `receiver` is `nil` and `name` is `:capitalize`. Fix the cause, not the symptom: use the non-bang method, `fetch` to fail loudly on missing data, or an explicit default, rather than `rescue nil`.
code
ruby · 14 linesrow = { title: " red mug " }
begin
row[:title].strip.squeeze!(" ").capitalize
rescue NoMethodError => e
e.receiver # => nil
e.name # => :capitalize
end
# Probe: tap passes the value through unchanged
row[:title].strip
.tap { |v| warn "after strip: #{v.inspect}" }
.squeeze(" ")
.capitalize # => "Red mug"go deeper
Recall that the error names the method called on nil, so the problem is the link just before it.
List which common methods return nil on a miss or when nothing changed, and use tap or named locals to see intermediate values.
Diagnose data-dependent nil failures with error_highlight, the exception's receiver and name, and production records, then fix the link so future failures are louder.
Establish conventions that keep nil from travelling silently through data pipelines, such as fetch for required fields and no blanket rescues.
## Read the error correctly A `NoMethodError` whose message ends in `for nil` means a method was **sent to `nil`**. In a chain such as ```ruby row[:title].strip.squeeze!(" ").capitalize ``` the message `undefined method 'capitalize' for nil` does **not** say `capitalize` is broken. It says the link **before** it, `squeeze!(" ")`, returned `nil`. That shift by one link is the first thing to get right. The quoted method name and the `for nil` wording are the Ruby 3.4 and later message format. ## Let Ruby point at the call - **error_highlight**, built into Ruby since 3.1 and on by default, adds the source line to the error and underlines the failing call with carets, for example under `.capitalize`. When the same method appears twice on one line, the carets say which one failed. It can be switched off with `--disable-error_highlight`. - The **backtrace** gives the file and line; in a leading-dot chain spread over several lines, match the underlined snippet to the right link. - In a `rescue` or an error tracker, `NoMethodError` inherits from `NameError`, so `e.receiver` returns the object the call was made on, here `nil`, and `e.name` returns `:capitalize`. ## List the links that can return nil Most methods in a cleaning chain always return an object, so the candidates are few. Check each link's documented return value: | Link | Returns `nil` when | |---|---| | `String#strip!`, `squeeze!`, `gsub!`, `capitalize!` | nothing was changed ("self or nil") | | `Hash#[]` | the key is missing | | `Array#first`, `last` without a count | the Array is empty | | `Array#find` / `detect` | no element matches | | `String#[]` with a Regexp | the pattern does not match | In the chain above, `squeeze!` returns `nil` for every title without doubled spaces, which is why the failure is **data-dependent**: some rows pass, others crash. ## Confirm with the data 1. Reproduce with the failing record, not a hand-written fixture. 2. Insert a `tap` between links: `.squeeze!(" ").tap { |v| warn "after squeeze!: #{v.inspect}" }`. `tap` passes the value through unchanged, so the chain still behaves the same. 3. Or break the chain into named locals for the investigation; the backtrace line then identifies a single call. 4. Once the link is known, remove the probes. ## Fix the cause - **Bang method in a chain**: switch to the non-bang form, `squeeze(" ")`, which always returns a String, or make the bang calls separate statements. - **Missing data**: if a title is required, use `row.fetch(:title)` so a missing key raises `KeyError` naming the key at the source; if it is optional, supply an explicit default such as `row.fetch(:title, "")`. - **No match**: decide what an absent match means and handle it at that link, for example with a `then` block that returns a fallback. - Avoid `rescue nil` or blanket `NoMethodError` rescues: they hide the next bug of the same kind and silently produce empty titles. Guarding every link with the safe-navigation operator is a separate tool with its own trade-offs; it propagates `nil` instead of explaining it. ## Why this is a senior question The mechanics are simple; the skill is operational. A senior engineer reads the message as "the previous link", uses the tooling Ruby already provides instead of guessing, reproduces with production data, and chooses a fix that makes the next failure louder rather than quieter. Two habits prevent the next occurrence. First, keep bang methods out of chains entirely, so the "self or nil" family can never feed a later link. Second, treat every lookup that returns `nil` on a miss as a decision point: either the value is required, and `fetch` should fail at the source, or it is optional, and the default belongs right there, next to the lookup, where a reader can see it.
- Why is `row.fetch(:title)` often a better fix than adding a default at the end of the chain?`Hash#fetch` raises `KeyError` at the link where the data is missing and names the key, so the error points at the cause. A default added after the chain, or a nil guard on each link, lets the missing value travel further and produces a wrong title instead of an error. Use `fetch` with a default only where a missing title is genuinely acceptable.
- What does error_highlight add when a line holds two calls of the same method?It prints the source line under the message and places carets under the specific call that failed, so `a.first.name + b.first.name` shows whether the left or the right `name` hit `nil`. The backtrace alone gives only the line number, which cannot distinguish the two.
saying these in an interview costs you the question
- The method named in the error, capitalize, is the one returning nil.
- Wrapping the chain in rescue nil is an acceptable fix.
- A tap probe changes what the chain returns.
- Only bang methods can put nil into a chain.
- Since the chain passed tests, the nil must come from a Ruby bug.