Finding out what called a Ruby method

A quick way to understand what is calling your code using the caller method

Sometimes when we’re debugging some Ruby code, it is good to know what code has called the method we’re looking at. It is something I routinely like to know.

There are a few different ways we can go about this, but the one I reach for more often than not is the Ruby kernel method caller.

From the above-linked documentation, caller;

Returns the current execution stack—an array containing strings in the form file:line or file:line: in method.

I like it because;

  • It is built into Ruby, so no additional gems needed
  • It is easy to learn how to use it

I think the best way to talk about this method is with an example.

require 'date'

class Cat
  def about
    puts "name: #{name}"
    puts "age: #{age}" 
  end

  private

  def name
    'Pixel'
  end

  def age
    DateOfBirth.find_age(Date.new(2020,02,03))
  end
end

class DateOfBirth
  def self.find_age(date)
    ((DateTime.now - date) / 365).to_i
  end
end

puts Cat.new.about

Here we have two classes, Cat which has some information about cats, and DateOfBirth which we’re using to try and calculate an age based on a date of birth.

Don’t worry too much about the specific code, and please please please don’t use this code to work out age based on date of birth.

In this code we’re calling Cat.new.about and we want to see the cat’s name and age returned to us.

The developer here has been quite lazy and has hardcoded everything, so the Cat class is always going to make a cat with a specific name and date of birth.

Let’s pretend we don’t know that, lets assume this is a massive project and Cat isn’t something we’ve ever had to look at.

What we’re looking at is the DateOfBirth class, there has been a bug report that no matter what Cat we’re dealing with, their age is always the same, and we’ve been told the find_age method is probably to blame.

We open up our code editor and look at the relevant code, copied below for ease;

class DateOfBirth
  def self.find_age(date)
    ((DateTime.now - date) / 365).to_i
  end
end

We can see that find_age is maybe being overly simplistic with how it calculates the age, since not every year has 365 days, but it doesn’t explain our bug.

Enter caller

If we update the find_age method to include puts caller and run our Cat.new.about we get the following output;

name: Pixel
caller.rb:16:in `age'
caller.rb:6:in `about'
caller.rb:27:in `<main>'
age: 4

The “name” and “age” lines are from our Cat code, we can ignore them, the important bits are the “caller.rb” lines.

From bottom to top, we can read this as all of the code executed on the way to our method, if we lay it out from bottom to top, we get;

  • caller.rb:27:in `<main>’
  • caller.rb:6:in `about’
  • caller.rb:16:in `age’

To break this down;

  • caller.rb is the name of the Ruby file
  • :x, eg :6 is the line number in that file
  • :in 'method', eg :in 'about' is the method that the line resides in

Then in our specific case, we’re saying three things happened.

  • <main> was called, which is our Cat.new.about code
  • Cat.new.about then went to line 6 in the about method, which calls age
  • Line 16 in our age method then calls our DateOfBirth.find_age method

Now we have something to go on, we know the caller of our code, so we can open up caller.rb and check on line 16, and sure enough we will see it is hard-coded and is the source of our bug! Huzzah!

Improving on caller

In our little example it is fine to call caller, there was never going to be that much output, but in a real application, especially one that uses a framework, these stacks get get pretty large.

caller[0]

If you just want to care about the very last thing to run before our method, you can use puts caller[0], in our case it would have returned caller.rb:16:in 'age' which is exactly what we needed.

caller(x, y)

You can also tell caller where you want to start from and how many lines to return.

For example puts caller(0, 2) will return from the last line which called our method plus one more, for a total of 2 lines. This can be useful when you want more context, but don’t want too many lines to get returned. I like to start with 5 and see if I need to tweak from there.

Prefer a video?

I’ve recorded a short video explaining this article and added it to YouTube. I know some folk much prefer to watch and listen than read!


Recent posts View all

Ruby

The best way to test model scopes in Rails

Learn about Rails scopes and how to best test them with both Rspec and Minitest

Ruby

Finding out what called a Ruby method

A quick way to understand what is calling your code using the caller method