Ruby's instance_eval Gotcha
Recently, I worked with the AfterCommitQueue
module at GitLab. It provides a simple way to run code after committing a transaction or immediately if no transaction is present. We often use this to run jobs after a model is saved and prevent running the job within a transaction.
class Service
def execute
record.after_commit do
# schedule job
end
record.save
end
end
This module is useful and may be worth a separate note one day. Anyway, it uses instance_eval
under the hood, which has a gotcha that is good to know. For example, let’s try to print the @params
instance variable of the Service
object within a given block:
class Record
def run_block(&block)
instance_eval(&block)
end
end
class Service
def initialize
@params = "My service params"
end
def execute
record = Record.new
# Gotcha: when the block runs
# it will not have access to `@params` of the `Service` object.
record.run_block do
puts @params
end
end
end
service = Service.new
service.execute # => ""
The code above will print out nothing. The reason is: instance_eval
uses the instance variables of the object it has been called on (the receiver), which is in our case the Record
object. Let’s modify the code above for demonstration purposes:
class Record
def initialize
# Setting `@params` on the receiver
@params = "My record params"
end
def run_block(&block)
instance_eval(&block)
end
end
# Service class stays the same
service = Service.new
service.execute # => "My record params"
Now it would output My record params
. This is often not what we want. The easiest way to use the variables within the block is to set the variables as part of the scope of the caller method:
class Record
def run_block(&block)
instance_eval(&block)
end
end
class Service
def initialize
@params = "My service params"
end
def execute
record = Record.new
# Put the param in the scope of the method
# to make it available within the block.
my_params = @params
record.run_block do
puts my_params
end
end
end
service = Service.new
service.execute # => "My service params"
Now we’d output My service params
as expected.