skip to content

In Ruby, what does the safe navigation operator &. return when the receiver is nil, and what does it not protect against?

level: juniorimportance: should knowfreq 50%

answer

  1. added in Ruby 2.3
  2. nil receiver: returns nil
  3. arguments not evaluated
  4. false still gets the call
  5. skips only the next call

basics

~10 s

obj&.method(args) returns nil without calling the method or evaluating its arguments when obj is nil. It skips only that one call, still calls methods on false, and does not rescue NoMethodError on other objects.

solid answer

~40 s

`&.`, added in Ruby 2.3, checks its receiver: if it is `nil`, the call is skipped, its arguments are not evaluated and the expression returns `nil`; otherwise the method is called normally. Its limits are the interview part. It skips **one** call only, so `bill&.split.first` still calls `first` on `nil` - each link that may be nil needs its own `&.`, which RuboCop's `Lint/SafeNavigationChain` checks. It tests for `nil` only, so `false&.upcase` raises `NoMethodError`, unlike `x && x.upcase`, which returns `false`. And it guards the receiver, not the method: calling a method the object does not define still raises. It also works with assignment, so `order&.note = "x"` and `order&.note ||= "x"` do nothing when `order` is `nil`. Overusing it hides where `nil` should have been handled.

code

ruby · 16 lines
ruby
bill = nil
bill&.split(3)            # => nil; split never runs

lookups = 0
bill&.split(lookups += 1)
lookups                   # => 0; arguments are not evaluated

begin
  bill&.items.first       # items skipped, then first called on nil
rescue NoMethodError => e
  puts e.message          # undefined method 'first' for nil
end

bill&.items&.first        # => nil
false && false.upcase     # => false
# false&.upcase           # NoMethodError: only nil is skipped

go deeper

for a junior

Recall that obj&.m returns nil instead of raising when obj is nil, and that it was added in Ruby 2.3.

for a middle

Explain the limits: one call skipped, arguments not evaluated, false still receives the call, and misspelt methods still raise.

for a senior

Push back on &. chains in review: decide where nil is legitimate, handle it there, and keep NoMethodError loud where nil means a bug.

for a principal

Treat widespread &. as a data-model signal and invest in explicit optional values or null objects where absence is common.

The **safe navigation operator** `&.` was added in Ruby 2.3 to replace the `x && x.method` pattern. It is simple to use and easy to over-trust, so interviewers ask about its exact semantics. ## What &. does `receiver&.name(args)` evaluates `receiver` once. Then: - If `receiver` is **`nil`**, the call is skipped, the **arguments are not evaluated**, any block is not run, and the whole expression is `nil`. - Otherwise, `name(args)` is called exactly as with a normal dot. ```ruby table = nil table&.reserve(expensive_lookup) # => nil; expensive_lookup never runs ``` ## The limits 1. **Only one call is skipped.** Ruby's documentation states it directly: `&.` skips only the next call. In `bill&.items.first`, a `nil` bill makes `bill&.items` return `nil`, and then `.first` is called on `nil` and raises `NoMethodError`. Write `bill&.items&.first` if every link may be nil. RuboCop's `Lint/SafeNavigationChain` cop reports an ordinary call chained after `&.`. 2. **Only `nil` is special.** `false&.upcase` calls `upcase` on `false` and raises `NoMethodError`. Every other object, including `false`, `0` and `""`, receives the call. 3. **It guards the receiver, not the method.** `order&.tottal` on a real order still raises `NoMethodError` for the misspelt name; `&.` is not a rescue. 4. **It is not a design fix.** A chain like `diner&.bill&.total&.round` hides which link was nil and why. Often the right answer is to make the value non-nil (a null object, a default, an early return) or to fail loudly. ## &. compared with x && x.method | Case | `x&.m` | `x && x.m` | |---|---|---| | `x` is `nil` | `nil` | `nil` | | `x` is `false` | calls `false.m` (usually raises) | `false`, no call | | `x` is an expression with side effects | evaluated once | evaluated twice | | arguments to `m` when `x` is `nil` | not evaluated | not evaluated | The single evaluation is a real advantage when the receiver is a method call such as `find_table(id)&.seat(guest)`; the `&&` version would look the table up twice. ## Assignment through &. Safe navigation also works on attribute assignment and abbreviated assignment: ```ruby order&.note = "window seat" # nothing happens if order is nil order&.note ||= "none" # likewise order&.tip += 5 # likewise ``` When `order` is not nil, these call the reader and setter as usual. ## How it differs from optional chaining elsewhere In JavaScript, optional chaining `a?.b.c` short-circuits the **entire rest of the chain** when `a` is null or undefined. Ruby's `&.` does not: the chain continues after the skipped call, which is why `bill&.items.first` raises. Developers moving between the two languages trip over exactly this. ## Where it fits Good uses: - optional associations or configuration values that are legitimately absent, such as `guest.loyalty_card&.number`; - the result of a lookup that returns `nil` on a miss, such as `Hash#[]` or a regular-expression match: `line.match(/table (\d+)/)&.captures`. Warning signs: - `&.` on a value that should never be nil - it turns a clear `NoMethodError` near the bug into a `nil` that fails far away; - long chains of `&.` - each one is a question about the data model that the code is avoiding; - `&.` on booleans - `false` still receives the call. ## Combining &. with defaults `&.` pairs naturally with `||` to supply a fallback value when anything in the chain is missing: `guest&.loyalty_card&.discount || 0` returns the discount, or `0` when the guest, the card or the discount is absent. Because `||` also replaces `false`, this pattern suits values that are never legitimately `false`; for booleans, handle `nil` explicitly instead. Wrapping the same idea in a small method with a clear name - `discount_for(guest)` - keeps the nil handling in one place rather than repeated at every call site. ## Summary - `&.` returns `nil` and skips the call, including argument evaluation, when the receiver is `nil`. - It affects only the next call; guard each nullable link separately. - `false` is not skipped; misspelt methods still raise. - Prefer designs that make `nil` rare over chains of `&.`.

  • In Ruby, what does `order&.note ||= "none"` do when order is nil and when it is present?
    When `order` is `nil`, nothing is evaluated or assigned and the expression is `nil`. When `order` is present, it behaves like `order.note || order.note = "none"`: it calls the reader and calls the setter only if the note is `nil` or `false`.
  • Why can a long chain like `diner&.bill&.total&.round` be a code smell?
    Each `&.` says that link may legitimately be nil, and when the result is nil you cannot tell which link failed. It usually means the data model allows too many optional states; a default, a null object or an early return with a clear error localises the problem instead of spreading nil through later code.

saying these in an interview costs you the question

  • &. makes every later call in the chain safe as well.
  • &. skips the call when the receiver is false.
  • The arguments are still evaluated when the receiver is nil.
  • &. rescues NoMethodError when the method does not exist.
  • x&.m and x && x.m behave identically in every case.