rubyguides

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

ParameterTypeDescription
keyobjectThe key to look up.
defaultobjectValue to return if key is not found. Optional.
blockprocBlock 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

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:

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:

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:

h = {}
h.fetch(:cache_ttl) { |k| ENV["DEFAULT_#{k.upcase}"] || 300 }

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

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

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

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:

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