This month I looked into debugging ruby code. While I usually can figure out the source of bugs, I’ve been thinking about how to debug code more efficiently. When I debug in ruby, I tend to rely on printing variables to the terminal. If the code is more complex, I step through the code with byebug
or binding.pry
. During this past month, I’ve been learning techniques that let me level up these skills.
Navigating code
I learned a few techniques to navigate the codebase faster and level up both printing and byebug
based debugging.
Printing methods
One technique that I already used but is still worth mentioning is to use p
instead of puts
. puts
calls to_s
on the object, which by default is the object class and id
. You can override the to_s
class to return detailed information about the object. The other option is to use p
, which calls .inspect
on the object. By default, inspect returns a string with the class, object_id, and instance variables. The output of p
can be difficult to parse if an object has many instance variables. In this case, you can use pp
, which stands for pretty print, and makes the output easier to read. pp
is also helps to format hashes and JSON objects.
Raising errors
Sometimes it can be difficult to find the print statements in the server logs. One option is to prepend print statements with strings like “!!!” and search for them on the server output. Another technique is to raise an exception immediately after the print statements. Then you can find the code faster as you know it happens right before the exception. Raising errors is useful if that section of code runs many times. You can use conditional logic to raise an error in the cases you want to investigate.
Freezing
If you want to know when an object is modified, you can freeze
it. Then whenever the object is modified, it will raise an exception. Freezing an object is a faster way to figure out which classes are modifying it.
Leveraging Ruby
Methods such as inspect
and pp
are useful but don’t always appear in beginner ruby tutorials. I’ve found that learning more about ruby has given me new tools for debugging Ruby code. There are many methods in ruby that are there to make it easier for developers to work with ruby.
Objects
For example, if you have a method that takes an input ( input_obj
) but it’s not clear what type of input it is. Normally, I would search the code base for all the locations that this method is invoked. In the calling method, you can figure out what is passed in as the input. A faster way to figure this out would be to run the code and do p input_obj.class.name
. That way, you know the exact class of the input. Everything in ruby is an object and inherits from the Object
class class. It has methods such as methods
, instance_variables
, responds_to?
that you can use to learn more about method inputs. Granted, you can figure out a lot of this information with inspect
.
The Object class also mixes the Kernel module, which has a caller
method. You can use caller
to get the calling stack for an object. caller
is a faster way to figure out who is calling a method instead of searching through the entire code base.
Method
In ruby, even methods are objects! You can determine where a method is implemented by calling source_location
on the method:
ClassName.instance_method(:method_name).source_location
Using source_location
is especially useful when the method name is common and is harder to search for in the code. If a method calls super
, you can use super_method
to get the Method
object for the super method:
ClassName.instance_method(:method_name).super_method
Inheritance Hierarchy
Sometimes, the source of bugs is due to objects extending many modules that change their behavior in unexpected ways. You can track when a module is added to an object with included
. You can overwrite included
to print information when a module is included on an object. Use method_added
to track when an instance method is added to a module. These methods help track down bugs related to metaprogramming.
Tracepoint
Tracepoint
allows you to trace the call stack for a piece of code. To see all the methods called while a code block run, you could trace the call stack with Tracepoint:
1
2
3
4
5
6
7
trace = TracePoint.new(:call) do |tp|
p[tp.path, tp.lineno, tp.defined_class, tp.method_id]
end
trace.enable
User.some_method
trace.disable
After you create a new tracepoint, you must enable it. When enabled, a tracepoint object will log all the methods calls until the trace is disabled. When you initialize a new tracepoint, it takes a block executes for each method call. The example above prints the file the method is located in (tp.path
), the line number tp.lineno
, the class tp.defined_class
, and the method tp.method_id
.
The logging for Tracepoint is quite verbose as it will also output the method calls for code in gems. Thus, Tracepoint is more useful for getting the general execution path for the code.
To reduce the output, you can use conditionals to only print in certain cases:
1
2
3
4
TracePoint.trace(:call) do |tp|
next unless tp.self.is_a?(User) # only print method calls for Users
# tracing logic
end
That way, you can see how the execution path for a particular object to see how it is used.
Tracepoint’s code is also less intuitive to write. Rather than memorizing the code, I’d save it in a snippet and copy it whenever I want to use it.
Reading gem source code
Sometimes, the code I’m interested in exists in a gem instead of the application code. Understanding gem code usually requires reading the gem documentation to figure out how the code work. If you can not find the information in the docs, you would have to read the source code. I can read the code on Github, but this can be tedious to navigate and search. Instead, you can do bundle open <gem_name>
to open the code for the gem in a text editor. It will open the version specified in the nearest Gemfile. That way, you can use your IDE to search and navigate the gem code. In your application code, you can use source_location
to find the location of a method defined in a gem! You can also use print statements and byebug
to debug the gem source code if needed. When you finish debugging, use gem pristine <gem_name>
to clean up any changes.
Conclusion
Debugging ruby goes beyond the use of print statements to trace code execution. There is a lot of built-in ruby functionality which can help you more effectively debug your code. As I dig deeper into ruby, I now consider how I can leverage what I learn to debug code.