Hash#dig
Overview
Hash#dig navigates a chain of keys through nested hashes and arrays, returning nil if any key in the chain is missing or any intermediate value is nil. It eliminates the repetitive h[:key] && h[:key][:nested] && h[:key][:nested][:value] pattern that was the standard approach before dig existed.
dig also works on arrays, so you can mix hash and array access in the same call.
That makes it a good fit for ruby code that reads nested data from APIs or configuration files. The method acts like a small accessor for nested data, which is easier to read than a long chain of [] calls and easier to maintain when the shape of the input changes.
Signature
dig(key, *keys) -> object | nil
Parameters
| Parameter | Type | Description |
|---|---|---|
key | object | First key to look up in the hash. |
*keys | object | Additional keys for nested hashes, or integer indices for arrays. |
Return Value
The value at the end of the chain, or nil if any key or index is not found.
Unlike Hash#fetch, dig never raises an exception for a missing key.
Basic Usage
Deep access in nested hashes
The simplest use of dig is reaching into a hash that you know has multiple levels. Passing a chain of keys reads left to right, matching the natural way you would describe the path yourself: first the database entry, then the primary node, then the host field.
config = {
database: {
primary: {
host: "db1.example.com",
port: 5432
}
}
}
config.dig(:database, :primary, :host) # => "db1.example.com"
config.dig(:database, :primary, :port) # => 5432
config.dig(:database, :replica, :host) # => nil (replica key missing)
The method handles deeply nested structures with the same simple syntax regardless of depth. You can reach into a hash three levels down as easily as one level, which keeps the code concise even when the data model is complex.
Mix with arrays
data = {
users: [
{ name: "Alice", roles: ["admin", "developer"] },
{ name: "Bob", roles: ["developer"] }
]
}
data.dig(:users, 0, :name) # => "Alice"
data.dig(:users, 0, :roles, 0) # => "admin"
data.dig(:users, 1, :roles, 0) # => "developer"
data.dig(:users, 99, :name) # => nil (index 99 does not exist)
Return nil instead of error
The most valuable feature of dig is that it never raises an exception when a key or index is absent. Instead of crashing, it quietly returns nil, which lets you write code that handles missing data without wrapping every access in a begin/rescue block.
response = { user: { address: nil } }
response.dig(:user, :address, :city) # => nil (address is nil, stops there)
response[:user][:address][:city] # => Error: undefined method `[]' for nil:NilClass
Common use cases
Parsing API responses
This pattern is especially common when consuming JSON APIs where the response shape is partially unknown. By chaining keys in a single dig call, you can safely extract deeply nested fields without defensive nil checks at every level of the hierarchy.
response = JSON.parse(http_body)
user_id = response.dig("data", "user", "id")
created_at = response.dig("data", "user", "metadata", "created_at")
status = response.dig("data", "status") # nil if absent, no error
API payloads often mix hashes and arrays, so dig keeps the traversal in one expression. That helps when the response is only partly known ahead of time and you still want a safe way to reach a nested field without writing a cascade of conditional checks.
Safe config access with deep defaults
config = {
logging: {
level: "info",
outputs: [
{ type: "stdout" }
]
}
}
log_level = config.dig(:logging, :level) || "warn"
second_output = config.dig(:logging, :outputs, 1) # nil if no second output
This style works well for configuration because missing data is usually acceptable at read time. You can pair dig with a fallback value or a later validation step depending on how strict the code needs to be. The combination of dig and || gives you a one-liner that reads naturally: try the path first, then use the backup.
Optional nested attributes
product = {
metadata: {
images: [
{ url: "https://example.com/img1.jpg", alt: "Front view" }
]
}
}
image_url = product.dig(:metadata, :images, 0, :url)
image_alt = product.dig(:metadata, :images, 0, :alt)
missing = product.dig(:metadata, :images, 5, :url) # => nil
When the structure is nested but optional, dig keeps the code from turning into a maze of conditionals. The traversal stays visible, and the method returns nil instead of forcing the caller to handle every missing step manually. This safe-by-default behavior is the main reason dig appears so often in code that ingests JSON or other loosely structured data.
Comparison with Alternatives
vs manual chained access
# Without dig — verbose and error-prone
user.dig(:profile, :settings, :theme) rescue nil
# or:
user[:profile] && user[:profile][:settings] && user[:profile][:settings][:theme]
# With dig
user.dig(:profile, :settings, :theme)
The dig version is shorter and also safer, because one missing key does not break the whole expression.
vs Hash#fetch
fetch raises KeyError when a key is absent. dig returns nil. When you need to distinguish between a missing key and a nil value, fetch is still the right tool:
h = { key: nil }
h.fetch(:key) # => nil (key exists with nil value)
h.dig(:key) # => nil
h.fetch(:missing) # => KeyError
h.dig(:missing) # => nil
Use fetch when a missing key should be treated as an error. Use dig when the path may or may not exist and nil is an acceptable result. The choice between them depends on whether you want to surface the absence of a key as an exception or continue with a soft default.
Array access mixed in
# dig can access array indices mid-chain
data = { items: [{ name: "First" }, { name: "Second" }] }
data.dig(:items, 1, :name) # => "Second"
With chained [] access, this works too but is less readable. The brackets hide the traversal order and make it harder to see at a glance which keys and indices are being accessed in what sequence. Nested brackets also carry a higher risk of NoMethodError when an intermediate value turns out to be nil unexpectedly.
data[:items][1][:name] # => "Second"
This is one of the places where dig is easier to scan than nested brackets. The same call can move across hashes and arrays without changing shape. The readability gain becomes more noticeable as the nesting depth increases — each additional bracket in the chained version adds visual noise that dig absorbs into its argument list.
Gotchas
nil stops the chain. If an intermediate value is nil, dig returns nil rather than continuing:
{ a: nil }.dig(:a, :b, :c) # => nil (stops at :a because it is nil)
This is the intended behavior — it prevents the undefined method '[]' for nil error — but it means you cannot distinguish between “key was nil” and “key was missing” using dig alone.
That tradeoff is why dig works best for read paths where the caller only needs “something or nothing”. If the code must distinguish between an explicit nil and a missing key, use fetch or a separate existence check instead.
Integer arguments are array indices. If the value at a key is an array, pass an integer to access by index:
{ list: ["a", "b", "c"] }.dig(:list, 1) # => "b"
{ list: ["a", "b", "c"] }.dig(:list, -1) # => "c" (negative index works too)
dig works on nil. Calling nil.dig(*keys) returns nil without error. This is useful when a variable might be nil because you can chain the dig call without first checking whether the receiver itself exists. This nil-safety extends to any value along the chain, making dig a resilient choice for data that may be incomplete.
response = some_api_call() # might return nil
response.dig(:result, :value) # => nil safely
See Also
- /reference/hash-methods/fetch/ — retrieve a value with explicit error handling when key is absent
- /reference/hash-methods/has-key/ — check whether a key exists without retrieving its value
- /reference/hash-methods/keys/ — get all keys from a hash