Hash#fetch
Overview
Hash#fetch retrieves the value for a given key, just like h[key]. The difference is what happens when the key is missing. Where h[key] returns nil, fetch gives you three options: raise an error, return a default value, or run a block.
This matters because nil is a valid hash value. If you store h[:answer] = nil and then check h[:answer] expecting it to mean “key not found”, you get a false positive. fetch disambiguates between a missing key and a nil value.
Signature
fetch(key) -> object
fetch(key, default) -> object
fetch(key) { |key| block } -> object
The three forms of fetch cover most practical situations. The single-argument form is the strictest and is best for required keys where absence is a genuine error. The two-argument form provides a clean fallback when a sensible default exists. The block form offers the most flexibility, letting you compute the fallback or perform additional actions like logging when a key is missing.
Parameters
| Parameter | Type | Description |
|---|---|---|
key | object | The key to look up. |
default | object | Value to return if key is not found. Optional. |
| block | proc | Block evaluated with the key as argument if key is not found. Optional. |
Return Value
The value associated with key, the default value, or the block’s return value.
Raises KeyError if key is not found and no default or block is given.
Basic usage
Raise on missing key
The most common form:
config = { host: "localhost", port: 5432 }
config.fetch(:host) # => "localhost"
config.fetch(:port) # => 5432
config.fetch(:user) # => KeyError: key not found: :user
When a key is required for the program to continue, the single-argument form surfaces missing keys immediately. This makes debugging faster because you see exactly which key was expected and where the lookup happened, instead of chasing a nil that appears several method calls later.
Providing a default value
Pass a second argument as the fallback:
options = { theme: "dark" }
options.fetch(:language, "en") # => "en"
options.fetch(:theme, "light") # => "dark"
The default is returned only when the key is absent, not when the value is nil. This distinction is the entire reason fetch exists: a stored nil is treated as a valid value, and the fallback only triggers when the key itself is not present in the hash:
h = { key: nil }
h.fetch(:key, "default") # => nil (the stored nil, not "default")
Using a block
Pass a block and it runs only when the key is absent. The block form gives you the most control because you can compute the fallback value dynamically, log the missing key, or even raise a custom error with a more descriptive message than the default KeyError:
data = { api_version: "v2" }
data.fetch(:timeout) { |k| puts "#{k} not found, using default"; 30 }
# => "timeout not found, using default"
# => 30
The block receives the missing key as its argument, which is useful for building dynamic defaults. You can use environment variables, configuration files, or computed values inside the block without polluting the call site with conditional logic. The block also receives the missing key itself, so the fallback can depend on which key was requested:
h = {}
h.fetch(:cache_ttl) { |k| ENV["DEFAULT_#{k.upcase}"] || 300 }
Each form of fetch has its place. The single-argument form catches missing keys during development and testing. The two-argument form works well for optional configuration values. The block form suits cases where the default depends on the key name or requires computation that would be wasteful to perform unless the key is actually absent.
Comparison with h[key]
h = { answer: nil }
h[:answer] # => nil (could mean missing, could mean stored nil)
h.fetch(:answer) # => nil (raises KeyError if you want to distinguish)
h[:missing] # => nil (silent failure — easy to miss)
h.fetch(:missing) # => KeyError (you know something is wrong)
Common use cases
Configuration with required keys
The examples below show fetch applied to realistic scenarios. Each one builds on the same core idea: decide what happens when a key is absent, and make that decision explicit in the code rather than relying on Ruby’s default nil return.
class AppConfig
def initialize(raw)
@config = raw
end
def database_url
@config.fetch(:database_url) do
raise "database_url is required — set it in your environment"
end
end
end
AppConfig.new({}).database_url
# => raises "database_url is required"
Safe API response parsing
When working with parsed JSON responses, nested fetch calls let you safely drill into optional sections without a cascade of nil checks. If any expected key is absent, the error points directly to the missing piece. The pattern works well when the API contract is known but the response shape can vary:
response = JSON.parse(http_response.body)
user_id = response.fetch("user", {}).fetch("id", nil)
created_at = response.fetch("user", {}).fetch("created_at", nil)
Defaults that differ per key
When your application has a shared set of defaults that individual callers can override, fetch with a block lets you fall through from the caller’s options to the system defaults in a single expression. This two-tier lookup is clean enough to read inline:
DEFAULTS = { timeout: 5, retries: 3, debug: false }
def fetch_option(key)
options.fetch(key) { |k| DEFAULTS.fetch(k) }
end
opts = { timeout: 10 }
fetch_option(:timeout) # => 10 (from options)
fetch_option(:retries) # => 3 (from DEFAULTS)
fetch_option(:unknown) # => KeyError (not in either)
Fetch with multiple values
Ruby also provides Hash#fetch_values, which fetches multiple keys at once and raises KeyError for any missing key. This is convenient when a method needs several values from a hash and you want to validate all of them at once rather than checking each one individually:
user = { name: "Alice", email: "alice@example.com", role: "admin" }
user.fetch_values(:name, :email)
# => ["Alice", "alice@example.com"]
user.fetch_values(:name, :missing_key)
# => KeyError: key not found: :missing_key
This is useful when you need several values in one call and want to fail fast if any are absent.
Gotchas
Block and default argument together. If you pass both a default argument and a block, the block is ignored:
h = {}
h.fetch(:k, "default") { |k| "block result" }
# => "default" (block is never called)
Subtle difference between fetch and h[key] when key exists with nil value. As shown above — fetch does not treat a stored nil as missing. Only an absent key triggers the default or the block.
Calling fetch on a hash subclass. Hash#fetch works normally on subclasses like HashWithIndifferentAccess from ActiveSupport.
See Also
- /reference/hash-methods/has-key/ — check whether a key exists without retrieving its value
- /reference/hash-methods/dig/ — safely dig into nested hashes and arrays
- /reference/hash-methods/values/ — get all values from a hash