Using `Hash#fetch` in Ruby for better nil handling

Raquel Moss

—

March 13, 2019

Using Hash#fetch in Ruby for better nil handling

Pulling values out of a Hash in Ruby is simple with the [] method, but problems can occur when the value you’re looking up isn’t there. This can result in cumbersome nil checks, or our absolute favourite error Undefined method for nil:NilClass.

Let’s look at an example of a classifieds site that sorts its listings when displaying them for the user, and some of the ways we can use Hash#fetch to proactively handle those nils before they happen.

Just a quick note on the code—these examples are moderately contrived and not necessarily how you’d solve these problems in production, but hey, at least they illustrate my points!

Using Hash#fetch to set a default

1def index

2 valid_sort_orders ={

3 lowest_price::asc,

4 highest_price::desc

5}

6

7 sort_order = valid_sort_orders[listings_params[:sort]]

8

9@listings=Listing.order(price: sort_order)

10end

We have a problem here—what if the sort parameter is not provided by the user, or they provide something that’s invalid? There’s many ways to handle this problem, for example, we could set a default with the || operator. This is a pretty common pattern, and a perfectly fine way to handle this case.

1def index

2 valid_sort_orders ={

3 lowest_price::asc,

4 highest_price::desc

5}

6

7 sort_order = valid_sort_orders[listings_params[:sort]]||:asc

8

9@listings=Listing.order(price: sort_order)

10end

Personally, I prefer to use the Hash#fetch method for a slightly more elegant solution. With Hash#fetch, if a key is not found in a hash, we can provide a default key to look for, which is quite a nice way to handle our nil situation.

1def index

2 valid_sort_orders ={

3 lowest_price::asc,

4 highest_price::desc

5}

6

7 sort_order = valid_sort_orders.fetch(listings_params[:sort],:asc)

8

9@listings=Listing.order(price: sort_order)

10end

Semantics alone aren’t a great reason to use this pattern though, so lets look at some more interesting examples where Hash#fetch can be used to proactively handle nils.

Using Hash#fetch to set a falsey value

This pattern is useful when you want to accept falsey values from a caller, but default to a truthy value. Let’s look at an example.

Here we want to be able to control with parameters whether to include a seller’s details in the response, with a default value of true. It’s easy to accidentally do something like this:

1def index

2@include_seller_details= listings_params[:include_metadata]||true

3

4 valid_sort_orders ={

5 lowest_price::asc,

6 highest_price::desc

7}

8

9 sort_order = valid_sort_orders.fetch(listings_params[:sort],:asc)

10

11@listings=Listing.order(price: sort_order)

12end

Which is not going to work correctly! Because if listing_params[:include_metadata] is a falsey value, this will evaluate to true anyway. It’s probably not disasterous, but it means that we’re going to be sending more information in the response than we want to, which is impolite at best and could be a security concern at worst.

In this example, if there is any value at :include_metadata, including a falsey value, it will be set, which is exactly what we want.

Run a block of code if you don’t find what you’re looking for

In the case where you don’t find what you’re looking for in a hash, returning a default value is nice, as we’ve seen. Sometimes, though, a simple value won’t do, and you might want to run a block of code as a fallback instead. Hash#fetch accepts a block to help you achieve this, which is pretty nifty!

In this example, if a user sends an invalid sort order to the API, we will record that in an analytics service. That will help us decide if we want to build that feature for our users next. If a lot of users are requesting to order Listings by popular or new, it’s handy for us to know that. So, we’ll report the value, then return the default.

Safer handling of environment variables with Hash#fetch

Poor handling of environment variables can make for some pretty disasterous outcomes (…ask me how I know). After being bitten more than once, I like to use Hash#fetch when retrieving environment variables.

This is especially important if you are using environment variables to feature flag releases, or if adding/removing environment variables is something that is handled separately to your normal code deploy process.

code expecting an environment variable + a silent failure if it’s not there + failing to correctly set an environment variable = potentially very costly mistake

:upside_down_face:

1def index

2 seasonal_discount =ENV["SEASONAL_DISCOUNT"]

3

4 valid_sort_orders ={

5 lowest_price::asc,

6 highest_price::desc

7}

8

9 sort_order = valid_sort_orders.fetch(listings_params[:sort],:asc)

10

11@listings=Listing.order(price: sort_order)

12

13if seasonal_discount

14@listings=@listings.map(&:apply_seasonal_discount)

15end

16end

In this case, if someone fails to set the SEASONAL_DISCOUNT environment variable, we might fail to notice because nothing here is going to throw an error. Our customers will miss out on a good deal!

We can use Hash#fetch so that this fails noisily when no env var is found.

1def index

2 seasonal_discount =ENV.fetch("SEASONAL_DISCOUNT")

3

4 valid_sort_orders ={

5 lowest_price::asc,

6 highest_price::desc

7}

8

9 sort_order = valid_sort_orders.fetch(listings_params[:sort],:asc)

10

11@listings=Listing.order(price: sort_order)

12

13if seasonal_discount

14@listings=@listings.map(&:apply_seasonal_discount)

15end

16end

If no environment variable is found, this will fail noisily and your code probably won’t even deploy correctly. You could also choose to set a default if it makes more sense to do so, but there are often cases when a noisy failure is preferable.

Those are some of my favourite uses for Hash#fetch. It's a really useful tool for any Rubyist, and I think it’s good to remember that [] is not the only way to retrieve values from a Hash.