Watch out for Ruby blocks scope gotcha
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.
By the way, here is my upcoming book!