Watch out for Ruby blocks scope gotcha

benjamin roth
2 min readSep 19, 2017

--

Blocks are probably one of the best parts of Ruby. They bring much of its flexibility and power.

They have their own scope and can be used to create closures.

This lead me to introduce a bug in an app recently. Here is somewhat how the code looked like:

def update
availability.update(availability_params)
metrics_tracker = MetricsTracker.build_for(activity)
metrics_tracker.save
if metrics_tracker.trigger_notification?
# send email to admin
end
# rendering code
end
# code omitted...def metrics_tracker
@metrics_tracker ||= MetricsTracker.new(current_user)
end

We had a first bug because there was no database transaction. So I did add one:

def update
ApplicationRecord.transaction do # <= added transaction here
availability.update!(availability_params)
metrics_tracker = MetricsTracker.build_for(activity)
metrics_tracker.save!
end
if metrics_tracker.trigger_notification?
# send email to admin
end
# rendering code
end

But then no more emails were sent to admins… Why?

Let’s look back at our transaction:

ApplicationRecord.transaction do
availability.update!(availability_params)
metrics_tracker = MetricsTracker.build_for(activity)
metrics_tracker.save!
end

We define metrics_tracker inside the block, but here it is a local variable, scoped to the block and the block only. Meaning outside the block, metrics_tracker is not defined…
As a result, the metrics_tracker method defined in the controller was triggered, returning a new object which did not know anything about the availability and thus responded false to trigger_notification? .

I would say the obvious reason of this bug is one should not use the same name for a local variable and a method…
But well we just inherit legacy code and have to deal with it.

In the end a quick fix was to do:

def update
ApplicationRecord.transaction do
availability.update!(availability_params)
@metrics_tracker = MetricsTracker.build_for(activity) # <= here!
metrics_tracker.save!
end
if metrics_tracker.trigger_notification?
# send email to admin
end
# rendering code
end
# code omitted...def metrics_tracker
@metrics_tracker ||= MetricsTracker.new(current_user)
end

Blocks let you change the object you are working on, here setting an instance variable. Once set, the metrics_tracker method returns what we expect thanks to the memoization.

An easy way to remember this would be to consider using a block is almost like using a separate method: you can work on your object but locals remain locals.

--

--

benjamin roth
benjamin roth

Written by benjamin roth

Ru(g)by fan, Ruby on Rails / Javascript freelancer, Haskell lover

Responses (3)