Skip to content

&. skips the call when the receiver is nil

Level: 101 · a first-week reader who writes x.y if x else None in Python

One line: receiver&.method(args) returns nil without calling anything — not even evaluating the arguments — when the receiver is nil, and only nil; dig does the same across nested Hashes and Arrays, and fetch is the strict opposite of the lenient [].

Calling a method on nil is the most common NoMethodError in Ruby, and &. (the "safe navigation" or "lonely" operator, in the language since Ruby 2.3 (Not machine-checked here.)) is the one-character answer: if the receiver is nil, the whole call is skipped and the expression is nil; if not, it is an ordinary call. Two details matter. The arguments are not evaluated when the call is skipped, so a side effect inside them does not happen. And only nil is skipped — false is an object with methods and is called normally. For nested data the same idea is Hash#dig and Array#dig, which stop at the first missing key and return nil instead of raising. The reverse tool is fetch: where h[:missing] quietly gives nil, h.fetch(:missing) raises KeyError, and fetch with a default or a block is the explicit way to say what a missing key means.

Verified output of safe_navigation_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. a method on nil:               name.length          -> NoMethodError: undefined method 'length' for nil
2. safe navigation:               name&.length         -> nil, "abc"&.length -> 3
3. the arguments are skipped:     nil&.concat(noisy)   -> nil
   ...and evaluated otherwise:    "x"&.concat(noisy)   -> (argument evaluated) "x!"
4. only nil is skipped:           false&.to_s          -> "false" (false is a real object with methods)
5. a chain of objects:            u1.address&.city     -> "Oslo", u2.address&.city -> nil
                                  u2.address.city      -> NoMethodError: undefined method 'city' for nil
6. dig through nested Hashes:     h.dig(:a, :b, :c)    -> 1, h.dig(:a, :x, :c) -> nil
                                  h[:a][:x][:c]        -> NoMethodError: undefined method '[]' for nil
7. [] is lenient, fetch is strict: h[:zz]              -> nil, h.fetch(:zz) -> KeyError: key not found: :zz
                                  h.fetch(:zz, "default") -> "default"
8. Array#dig and a dead end:      [[1, [2]]].dig(0, 1, 0) -> 2, [].dig(0, 1) -> nil
                                  h.dig(:a, :b, :c, :d) -> TypeError: Integer does not have #dig method
9. &. guards one call only:       nil&.length.zero?    -> NoMethodError: undefined method 'zero?' for nil
                                  nil&.length&.zero?   -> nil

Reading the output

Row 1 is the error &. exists for: undefined method 'length' for nil. Row 2 is the operator on a nil and on a String. Row 3 shows the argument being skipped: nil&.concat(noisy) prints nothing, while "x"&.concat(noisy) prints "(argument evaluated)" before the result. Row 4: false&.to_s is "false", because only nil short-circuits. Row 5 is the everyday shape — a chain of objects where one link may be nil — with the unguarded version's error beside it.

Rows 6 to 8 are the Hash and Array tools. dig walks :a, :b, :c and returns nil at the first missing key, where h[:a][:x][:c] fails on nil[]; [] on a missing key is nil, fetch raises KeyError: key not found: :zz, and fetch with a default returns it. Array#dig works the same way, and dig into something that is not a container (the 1 at the end of the chain) is a TypeError, not a silent nil. Row 9 is the detail a JavaScript reader needs: &. guards one call. In nil&.length.zero? the &. returns nil and then .zero? is called on that nil normally — so each link that may be nil needs its own &..

Compared with Python

Python has no safe-navigation operator; PEP 505 proposed ?. and ?? and was deferred (Not machine-checked here.). The twin uses what is there. The conditional expression (row 2) is the direct translation and, like &., it skips the arguments (row 3); getattr(x, "y", None) does not — it evaluates the call's arguments first, as the printed "(argument evaluated)" shows. Row 4 is the trap in the shorter x and x.upper() idiom: and drops every falsy value, so an empty string never gets its method called, where Ruby's &. and the is not None test skip only None. Rows 6 and 7 are the mirror image of Ruby's defaults: d[k] is strict and raises KeyError, d.get(k) is lenient and returns None, so a chain of .get(k, {}) is Python's dig. There is no dig itself; row 8 writes it in five lines. Row 9 has nothing to measure: each step in a chain needs its own guard.

Verified output of safe_navigation_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. a method on None:              name.upper()         -> AttributeError: 'NoneType' object has no attribute 'upper'
2. the conditional expression:    name.upper() if name is not None else None -> None ; 'abc' -> 'ABC'
3. the arguments are skipped:     x.replace('a', noisy()) if x is not None else None -> None
   ...but not by getattr:         getattr(x, 'replace', lambda *a: None)('a', noisy()) -> (argument evaluated) None
4. only None is skipped:          '' if '' is None else 'kept'  -> 'kept'; but '' and ''.upper() -> '' (the `and` idiom drops every falsy value)
5. a chain of objects:            getattr(u1.address, 'city', None) -> 'Oslo' , getattr(u2.address, 'city', None) -> None
                                  u2.address.city      -> AttributeError: 'NoneType' object has no attribute 'city'
6. get chains through dicts:      h.get('a', {}).get('b', {}).get('c') -> 1 , with 'x' -> None
                                  h['a']['x']['c']     -> KeyError: 'x'
7. [] is strict, get is lenient:  h['zz']              -> KeyError: 'zz' , h.get('zz') -> None
                                  h.get('zz', 'default') -> 'default'
8. no dig: a five-line helper:    dig([[1, [2]]], 0, 1, 0) -> 2 , dig([], 0, 1) -> None
                                  dig(h, 'a', 'b', 'c', 'd') -> None (the helper swallows the dead end)
9. no operator to chain:          PEP 505's None-aware `?.` was deferred; each step needs its own guard

Try it

  1. Given users = [{name: "ann", address: {city: "Oslo"}}, {name: "bo"}, {name: "cy", address: nil}], print each user's city or nil/"(none)" three ways: with &., with dig, and with (u[:address] || {}).fetch(:city, "(none)").
  2. Write city_of(user) that returns the city or raises KeyError with your own message when the address is missing, using fetch with a block. Then show nil&.length.zero? and nil&.length&.zero? side by side and explain, from the printed error, which call the &. protected.
Solution to 1

safe_navigation_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Kata: each user's city or "(none)", three ways: &., dig, and || {} + fetch.
users = [
  { name: "ann", address: { city: "Oslo" } },
  { name: "bo" },
  { name: "cy", address: nil },
]

puts "1. with &.:"
users.each { |u| puts "   #{u[:name]}: #{(u[:address]&.fetch(:city, nil)).inspect}" }
puts "2. with dig:"
users.each { |u| puts "   #{u[:name]}: #{u.dig(:address, :city).inspect}" }
puts "3. with || {} and fetch's default:"
users.each { |u| puts "   #{u[:name]}: #{(u[:address] || {}).fetch(:city, "(none)")}" }

Verified output of safe_navigation_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. with &.:
   ann: "Oslo"
   bo: nil
   cy: nil
2. with dig:
   ann: "Oslo"
   bo: nil
   cy: nil
3. with || {} and fetch's default:
   ann: Oslo
   bo: (none)
   cy: (none)

See also