In Ruby, how does Hash#fetch differ from Hash#[] when a key is missing, and what do fetch's default argument and block do?
answer
- [] consults the hash default
- fetch raises KeyError
- KeyError#key and #receiver
- fetch ignores Hash.new defaults
- block beats default, with a warning
basics
~20 sHash#[] returns the hash's default, nil unless one was set, for a missing key. Hash#fetch raises KeyError instead, or returns its second argument or its block's value, and it ignores the hash's default and default proc.
solid answer
~40 s`review[:rating]` returns `nil` for a missing key, or the hash's default if `Hash.new` set one, so it cannot tell a missing key from one stored as `nil`. `review.fetch(:rating)` raises `KeyError` with `key not found: :rating`, and the exception's `key` and `receiver` say what was missing where. `fetch(:rating, 0)` returns `0` on a miss, and `fetch(:rating) { |key| ... }` runs the block only on a miss, which matters when the fallback is expensive because a default argument is evaluated on every call. Passing both warns `block supersedes default value argument` and uses the block. `fetch` never consults the hash's default or default proc. Use `fetch` for required keys such as configuration, and `[]` or `dig` for optional ones.
code
ruby · 18 linesreview = {title: "Dune", rating: 5, body: nil}
review[:body] # => nil (stored)
review[:author] # => nil (missing)
review.key?(:author) # => false
review.fetch(:rating) # => 5
review.fetch(:stars, 0) # => 0
review.fetch(:stars) { |k| "no #{k}" } # => "no stars"
begin
review.fetch(:author)
rescue KeyError => e
e.message # => "key not found: :author"
e.key # => :author
end
Hash.new(0).fetch(:views, 7) # => 7, the hash default is ignoredgo deeper
Recall that [] returns nil or the hash default on a miss while fetch raises KeyError, and that fetch takes a fallback argument or block.
Explain that fetch ignores Hash.new defaults, that its default argument is evaluated eagerly while the block is lazy, and how key? separates missing from nil.
Use fetch at trust boundaries so bad payloads and missing settings fail where they are read, and report KeyError#key rather than parsing messages.
Set a team convention for required versus optional hash reads so failures surface early without scattering nil checks through the codebase.
## Two ways to read a key A Ruby `Hash` offers two everyday readers with opposite attitudes to a missing key: - **`Hash#[]`** is lenient. A missing key returns the hash's **default**, which is `nil` unless the hash was built with `Hash.new(value)` or a default block. - **`Hash#fetch`** is strict. A missing key raises **`KeyError`** unless the call itself supplies a fallback. Take a book review parsed from a form: `review = {title: "Dune", rating: 5, body: nil}`. ## [] and the ambiguity of nil `review[:body]` and `review[:author]` both return `nil`, but only one key exists. When `nil` is a legitimate stored value, `[]` cannot distinguish "absent" from "present and empty". `review.key?(:body)` (also spelled `include?`, `member?`, `has_key?`) answers the presence question directly. `[]` also honours the hash default: on `counts = Hash.new(0)`, `counts[:missing]` returns `0`. The same is true of `dig` and `values_at`. ## fetch and KeyError `review.fetch(:author)` raises `KeyError` with the message `key not found: :author`. The exception carries two readers: - **`KeyError#key`** returns the missing key, `:author`. - **`KeyError#receiver`** returns the hash that was searched. These make error reports precise without parsing the message. Crucially, `fetch` **ignores the hash's default and default proc**: `Hash.new(0).fetch(:missing)` still raises. The Ruby documentation says so explicitly, and it catches people who assume a counting hash is safe to `fetch` from. ## Typos surface with fetch A misspelled key is the most common reason a hash read goes wrong. `review[:ratng]` quietly returns `nil`, and the bug shows up far away as a wrong total or a `NoMethodError` on `nil`. `review.fetch(:ratng)` fails on the spot, and because Ruby loads the bundled `did_you_mean` library by default, the error's full message adds a suggestion such as `Did you mean? :rating` when a close key exists. Rescue `KeyError` specifically where a missing key is expected, rather than a broad `rescue`, so real typos still reach your error reports. ## Fallbacks supplied at the call site | Call | Key present | Key missing | |---|---|---| | `h[k]` | value | hash default, usually `nil` | | `h.fetch(k)` | value | raises `KeyError` | | `h.fetch(k, fallback)` | value | `fallback` | | `h.fetch(k) { \|key\| ... }` | value, block not run | block's return value | Two details matter in practice: 1. **The default argument is always evaluated.** `h.fetch(k, load_from_disk)` calls `load_from_disk` before `fetch` runs, even when the key exists. The block form runs only on a miss. 2. **Passing both** a default and a block prints the warning `block supersedes default value argument`, and the block wins. `fetch_values(:a, :b)` is the multi-key strict reader: it raises `KeyError` for any missing key, or calls its block for each. `values_at(:a, :b)` is the lenient one and returns defaults. ## Nested data: dig versus chained fetch For nested data, such as a review with an `author` sub-hash: - **`review.dig(:author, :name)`** returns `nil` as soon as any level is missing, or the hash default at that level. It raises `TypeError` only when an intermediate value has no `dig` method, for example a string. - **`review.fetch(:author).fetch(:name)`** raises `KeyError` naming exactly which key was missing. - **`review[:author][:name]`** raises `NoMethodError` on `nil` when `author` is missing, which is the least informative of the three. ## Choosing - **Required keys**, such as configuration or a payload's mandatory fields: `fetch`, so a typo or a missing setting fails at the line that needs it. - **Optional keys** with a sensible absence value: `[]`, or `fetch(key, fallback)` to make the fallback visible. - **Expensive fallbacks**: the block form of `fetch`.
- Why prefer fetch(key) { ... } over fetch(key, default) when the fallback is expensive?Ruby evaluates every argument before calling the method, so `fetch(:cover, load_cover)` runs `load_cover` even when `:cover` is present. The block is only called on a miss. Use the block for anything that allocates, queries or has side effects; a literal like `0` or `""` is fine as an argument.
- How do you read a nested review field so that absence gives nil, and how so that it gives an error?`review.dig(:author, :name)` returns `nil` when either level is missing, or the hash default at that level. `review.fetch(:author).fetch(:name)` raises `KeyError` naming the missing key. Choose `dig` for optional data and chained `fetch` where the field is required.
- Does fetch use the default of a hash built with Hash.new(0)?No. `fetch` ignores both the default value and the default proc, so `Hash.new(0).fetch(:missing)` raises `KeyError`. Only `[]`, `dig` and `values_at` consult the hash's default.
saying these in an interview costs you the question
- Hash#fetch returns nil for a missing key, just like []
- fetch falls back to the default set with Hash.new(0)
- A missing key makes Hash#[] raise KeyError
- fetch(:k, expensive_call) only runs expensive_call when the key is missing
- review[:body] returning nil proves the body key is absent
- Hash#dig raises KeyError when a nested key is missing