Hash#dig
hash.dig(*keys) -> value or nil Dig safely retrieves nested values from hashes and arrays. It walks through the structure using each argument as a key, returning nil if at any point a key is missing instead of raising an error.
Compared with manual guard chains, dig keeps the nil checks inside the method call. The resulting expression is simpler to scan when the data is several levels deep or when hashes and arrays are mixed together.
In practice, it is especially helpful when you are reading data from a configuration file, an API response, or any other nested document where some branches might not exist. In those cases, dig keeps the lookup compact and lets the missing-data case stay inside the expression instead of spilling into the surrounding code.
Signature
hash.dig(key, *keys) → value or nil
Parameters
| Parameter | Type | Description |
|---|---|---|
key | Object | The first key to look up |
*keys | Object | Additional keys for nested access |
Return Value
Returns the value at the nested key path, or nil if any key in the path is missing.
basic usage
user = {
name: "Alice",
address: {
city: "London",
zip: "SW1A 1AA"
}
}
user.dig(:address, :city) # => "London"
user.dig(:address, :country) # => nil
user.dig(:phone) # => nil
The method stops at the first missing key and returns nil, never raising an error.
That behavior makes the method easy to use in read-only code. The caller can decide whether nil should mean “missing” or whether it should be converted into a fallback value with ||.
One nice side effect is that the lookup reads the same way you would describe the path in plain language. That helps when the nesting is deep enough that bracket chains become noisy or fragile.
working with arrays
.dig works with arrays too, using integer indices:
data = {
users: [
{ name: "Alice", scores: [95, 87] },
{ name: "Bob", scores: [78, 92] }
]
}
data.dig(:users, 0, :name) # => "Alice"
data.dig(:users, 1, :scores, 0) # => 78
data.dig(:users, 5, :name) # => nil
data.dig(:users, 0, :age) # => nil
Negative indices work as expected:
This matters when the data behaves like a ring or a queue, because the last item can still be reached with the same lookup style. It keeps the method consistent with normal array access, which makes the mental model easier to reuse in other parts of the code.
data.dig(:users, -1, :name) # => "Bob"
Mixed containers are common in real payloads, and dig keeps the lookup path readable even when the structure has a few layers.
Lists often hold hashes, and hashes often point to lists. dig follows that shape naturally, which means the code can mirror the data instead of translating it into a different access pattern first.
A visible path is easier to review at a glance. You can see where the walk starts, which branch it follows, and where it should stop if a key or index is missing.
When you are debugging a response from another system, a single method call makes it clear which part of the payload you expected to exist. That saves you from unrolling several guard clauses just to find the first absent branch.
the problem .dig solves
Before .dig (introduced in Ruby 2.3), you had to manually check each level:
# Old way - tedious and error-prone
if user[:address] && user[:address][:city]
city = user[:address][:city]
end
# With dig - clean and safe
city = user.dig(:address, :city)
You could also use the && operator, but it gets unwieldy with deep nesting:
The example below shows why the operator-based version falls apart as soon as the path gets longer. Each extra guard repeats the same prefix, so the code gets harder to scan even though the underlying idea is still simple.
# Works but hard to read with multiple levels
city = user[:address] && user[:address][:city] && user[:address][:city][:name]
That guard chain is the exact kind of structure dig replaces. Once the nesting gets deep, the manual checks become harder to scan, while dig keeps the access path short and direct.
The payoff grows as the access path gets longer. A lookup that would otherwise need several && checks turns into a single expression, and the caller can focus on the fallback behavior instead of the guard mechanics.
common use cases
Configuration Reading
config = {
database: {
production: {
host: "db.example.com",
port: 5432
}
}
}
config.dig(:database, :production, :host)
# => "db.example.com"
config.dig(:database, :staging, :host)
# => nil
Configuration lookups are one of the most natural uses for dig because missing values are common and often expected. Returning nil makes it easy to add a default later if the caller wants one.
That is especially helpful when one environment has a complete configuration and another one only fills in a subset of keys. The same lookup expression still works in both places, which keeps configuration code predictable.
api response parsing
response = {
data: {
user: {
profile: {
avatar_url: "https://example.com/avatar.png"
}
}
}
}
response.dig(:data, :user, :profile, :avatar_url)
# => "https://example.com/avatar.png"
API payloads tend to be nested and inconsistent, so a single method call that can tolerate missing branches is often the cleanest option. It also keeps the parsing logic close to the field you are actually reading.
Because the access path stays visible, it is easy to see which level of the payload matters. That clarity helps when API responses evolve and one branch changes shape while the rest of the document stays the same.
default values with nil coalescing
city = user.dig(:address, :city) || "Unknown"
This pattern is common because it separates lookup from fallback. dig gets the nested value, and the || operator handles the default in one readable step.
The two pieces work well together because each one has a single job. dig answers the question “what is at this path?” and || answers “what should happen if it is missing?”
When you combine them, the code stays compact without hiding the fallback rule. That makes the pattern easy to scan in a controller, a script, or any other place where missing nested data should map to a simple default.
comparison with bracket notation
| Aspect | Bracket Notation | .dig |
|---|---|---|
| Missing key | Raises KeyError | Returns nil |
| Deep nesting | Manual checks needed | Single call |
| Readability | Degrades with depth | Stays clean |
# Bracket notation - raises error
user[:address][:city] # => KeyError if :address missing
# dig - safe
user.dig(:address, :city) # => nil
dig is the safer choice when the lookup path is optional. Bracket notation is still useful when you want a missing key to fail fast, but the two styles communicate very different expectations to the reader.
When you are reading configuration, API responses, or other nested documents, dig keeps the access path compact and honest. It says exactly which levels matter and stops as soon as the path breaks, which makes it a strong default for read-only lookup code.
See Also
hash-fetch— Fetch a value with a default or blockhash-each— Iterate over key-value pairs