Last updated on July 28th, 2024 at 07:16 pm
Table of Contents
- Printing to the Console
- Interactive Debugging
- Tapping
- Determining Your Call Stack (looking back)
- Determining Your Call Path (looking forward)
- TracePoint
As with any programming language, there will eventually be situations where the program you write doesn’t behave as you expect and you will need to debug your code.
Printing to the Console
The first and easiest debugging mechanism in almost any programming language is writing strings to the console. Ruby has several commands which achieve this goal:
puts
The puts
statement will output the provided values to the console, terminating the output with a newline.
my_var = 12
puts "My assignment variable is #{my_var}" # Prints "My assignment variable is 12" to the console and returns a nil
p
The p
statement is the same as the puts
statement, except that it returns the value that was printed, whereas puts
always returns nil
.
my_var = 12
p "My assignment variable is #{my_var}" # Prints "My assignment variable is 12" to the console and returns the same string
pp
The pp
statement is a nice way to get an object to “Pretty Print”, separating elements of the object on their own line so that it is more readable, similar to the JavaScript function JSON.stringify(object, null, 2)
. If you print an array or hash, however, it will print on one line.
my_hash = {first: 'first', name: 'name', is: 'is'}
pp my_hash # Prints {first: 'first', name: 'name', is: 'is'} to the console and returns the same string
The print
statement is the same as the puts
statement, except that it does not add a newline at the end of the output.
Interactive Debugging
While a programmer can can solve a lot of simple problems with printing to the console, eventually, there is a need for more in depth debugging tools, which are commonly referred to as Interactive Debugging Tools. The most commonly used tools for this purpose are pry and byebug.
Pry and Byebug are both popular Ruby debugging tools, but they serve slightly different purposes and offer distinct features. A basic comparison of the two looks like:
- Interactive REPL Environment:
- Pry: Offers an interactive Read-Eval-Print Loop (REPL) environment, allowing you to experiment with code, inspect objects, and evaluate expressions on the fly. This makes it more versatile for exploration and debugging.
- Byebug: Primarily focuses on traditional debugging tasks, such as setting breakpoints, stepping through code, and inspecting variables.
- Code Navigation and Editing:
- Pry: Provides advanced code navigation features, such as the ability to list methods, show method source code, and navigate directly to method definitions. It also allows in-session editing of methods and the ability to reload code without restarting the session.
- Byebug: Does not offer built-in code navigation or editing capabilities. It focuses on runtime debugging and requires a different tool or method for code editing.
- Custom Commands and Plugins:
- Pry: Supports the creation of custom commands and plugins, enabling users to extend its functionality. This allows for a highly customizable debugging experience tailored to specific needs or workflows.
- Byebug: While it has a powerful set of built-in commands for debugging, it does not support the same level of extensibility with custom commands and plugins.
- Syntax Highlighting:
- Pry: Includes syntax highlighting for better readability of code and outputs, making it easier to distinguish different elements within the code.
- Byebug: Does not provide syntax highlighting, making it less visually appealing and harder to read, especially with complex code.
- Pry Hooks:
- Pry: Allows the use of hooks to customize the behavior of the REPL, such as executing specific code before or after the REPL starts. This adds another layer of customization and automation.
- Byebug: Does not have an equivalent feature for hooks, limiting the ability to automate and customize the debugging session.
In summary, Pry offers a more interactive and customizable debugging experience with its REPL environment, code navigation and editing features, custom commands and plugins, syntax highlighting, and hooks. Byebug, on the other hand, is more focused on traditional debugging tasks and does not provide the same level of interactivity and customization as Pry.
The tems have been combined into the gem pry-byebug.
Tapping
Chaining methods is one of the very powerful tools that Ruby provides. However, there are times that you need to see the values that are passed in a method chain. In this case, the tap command is exactly what you need.
class User
attr_accessor :name, :email
def initialize(name, email)
@name = name
@email = email
end
def send_welcome_email
# Imagine email sending logic here
end
end
user = User.new("Alice", "alice@example.com")
.tap { |u| puts "Created user #{u.name} with email #{u.email}" }
.send_welcome_email
# generates the output "Created user Alice with email alice@example.com"
Determining Your Call Stack (looking back)
Sometimes when debugging code, it is necessary to determine the call chain. The method caller can be used for this purpose. An example of using caller would be:
def third_method
puts "Inside third_method"
puts "Caller stack trace:"
puts caller
end
def second_method
puts "Inside second_method"
third_method
end
def first_method
puts "Inside first_method"
second_method
end
puts "Starting execution"
first_method
Which would generate the output
Starting execution
Inside first_method
Inside second_method
Inside third_method
Caller stack trace:
path/to/script.rb:5:in `second_method'
path/to/script.rb:10:in `first_method'
path/to/script.rb:14:in `<main>'
Determining Your Call Path (looking forward)
While caller can be used to determine where your method was called from, sometimes method names are duplicated in an application and you need to know which of the methods you are accessing when you call it. For this. you would use the method source_location. It returns to you the name of the file and the line number in that file where the method begins.
# my_class.rb
class MyClass
public
def method1
method2
end
def method2
method3
end
def method3
puts "Inside method3"
end
end
object = MyClass.new
object.method(:method1).source_location # Returns ["my_class.rb(irb), 7]
object.method(:method2).source_location # Returns ["my_class.rb(irb), 11]
object.method(:method3).source_location # Returns ["my_class.rb(irb), 15]
TracePoint
Ruby has a very interesting class called TracePoint which can be used to trace code execution. To use TracePoint you create a TracePoint object, specifying a filter which is to be used in the trace, and then enable the trace. Once it is enabled, each time the TracePoint is reached, it will execute the block defined in TracePoint creation.
An example taken from the ruby docs follows:
trace = TracePoint.new(:raise) do |tp|
p [tp.lineno, tp.event, tp.raised_exception]
end
#=> #<TracePoint:disabled>
trace.enable
#=> false
0 / 0
#=> [5, :raise, #<ZeroDivisionError: divided by 0>]
At the time of the writing this document, the available filters are:
Filter Name | Filter Function |
:line | execute an expression or statement on a new line |
:class | start a class or module definition |
:end | finish a class or module definition |
:call | call a Ruby method |
:return | return from a Ruby method |
:c_call | call a C-language routine |
:c_return | return from a C-language routine |
:rails | raise an exception |
:b_call | event hook at block entry |
:b_return | event hook at block ending |
:a_call | event hook at all calls (call , b_call , and c_call ) |
:a_return | event hook at all returns (return , b_return , and c_return ) |
:thread_begin | event hook at thread beginning |
:thread_end | event hook at thread ending |
:fiber_switch | event hook at fiber switch |
:script_compiled | new Ruby code compiled (with eval , load or require ) |