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
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 the key is missing and no fallback is provided, fetch raises KeyError immediately. This is the simplest and safest form: it tells you unambiguously that something expected was not there. For cases where a missing key should not halt execution, the two-argument form offers a concise alternative.
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 is one of the key differences between a default argument and a post-check: fetch distinguishes between “not present” and “present but nil,” so you can store nil as a meaningful value without having it mistaken for a missing key. That separation is what makes fetch safer than bracket access when nil has semantic meaning in your data:
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. Unlike the default argument, the block gives you full control over what happens when a key is missing, including logging the event, computing a fallback from other data, or raising a custom error with a descriptive message. The block receives the missing key as an argument, so you can build context-aware defaults without inspecting the hash separately:
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. Because the block only runs when the key is absent, expensive fallback computations stay lazy — you never pay for them on a cache hit. This also means the block can safely reference external state like environment variables or database lookups without worrying about the cost when the key is already present:
h = {}
h.fetch(:cache_ttl) { |k| ENV["DEFAULT_#{k.upcase}"] || 300 }
The block form is especially useful when the fallback value depends on which key is missing, as shown above. The block receives the missing key as an argument, so you can construct a key-specific default without writing a separate lookup table or a long chain of conditionals. This keeps the intent local to the fetch call instead of spreading defaulting logic across multiple methods or configuration files.
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)
The structural difference between bracket access and fetch is not just about the default value — it is about whether a missing key is treated as a normal condition or an error. Bracket access is convenient when nil is an acceptable stand-in for “not found,” but fetch is the right choice when the key should be present and its absence means something went wrong. That distinction is especially important in configuration loading, where a missing key often represents a deployment mistake, not a runtime decision.
Common use cases
Configuration with required keys
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"
This pattern makes missing configuration loud instead of quiet, which is the main reason to choose fetch over bracket access. When a key is absent and there is no sensible fallback, raising immediately surfaces the problem at startup instead of letting it propagate as a confusing nil later in execution. The custom error message in the block also gives future developers enough context to fix the issue without digging through stack traces.
Safe API response parsing
response = JSON.parse(http_response.body)
user_id = response.fetch("user", {}).fetch("id", nil)
created_at = response.fetch("user", {}).fetch("created_at", nil)
Chaining fetch on a nested hash with a default empty hash avoids NoMethodError when the outer key is missing. The inner fetch("id", nil) then safely returns nil instead of raising, which is often what you want when a field is optional. This pattern is common when parsing API responses where some keys may be absent at any level of nesting.
Defaults that differ per key
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. Unlike chaining individual fetch calls, fetch_values fails fast: it checks all requested keys up front and raises on the first one that is absent, instead of returning partial results. This is useful when you need several values in one call and want to fail fast if any are absent:
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