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.