ruby

Debugging in Ruby with Debug

Thomas Riboulet

Thomas Riboulet on

Debugging in Ruby with Debug

Debugging is a valuable skill for any software engineer to have. Unfortunately, most software engineers are not trained in it. And that's not just specific to developers going through boot camps; even in universities, we are not often taught and trained to use a debugger.

My teachers and mentors were more interested in getting me to write programs rather than debugging them. If we are fortunate, debugging comes at the end of the semester, in a short, last session.

Luckily, we have tools that can help us with debugging. Since Ruby 3.1, Ruby ships with the debug gem, a powerful debugger.

In this article, we will go through a quick overview of the gem. We'll see how to use it for simple and more advanced cases.

Debugging Without A Debugger: What's the Issue?

Many of us rely on what you might call "printf debugging": we add puts (or its equivalent in the language we're using) to the standard output (STDOUT). We include the current state of an object, variable, or just a string so we know if our program is going into specific branches of its logic tree.

While helpful, this isn't the most optimal way to debug a program. It often leads to many back-and-forth trips between your logs and the code, as you forget to add a puts here and there, or leave in some debugging code.

That method also relies on your own preconceptions about how the code is running and what is going on that's different from what you might expect.

Using a debugger is a very different experience. You add one or more breakpoints in the code where you want to know what's happening. You then run the code and wait for it to hit the breakpoint.

Then, you get a debugging console to check a variable's values at the breakpoint location. You go back and forth in the execution steps.

As we will see later, we can even add conditional breakpoints directly from the debugging console. This makes it easier to avoid exiting the debugging console, so you can add breakpoints you've forgotten about.

Setup

Since Ruby 3.1, a version of the debug gem ships with Ruby. We recommend adding it to your Gemfile so you're using the latest version.

Add debug to your Gemfile and then run bundle install. I recommend adding it to development and test groups for debugging tests too.

Basic Debugging Techniques with Debug for Ruby

Now let's run through some simple debugging methods using debug: using breakpoints, stepping, other commands, moving in the stack, and using a map. We'll then examine the more advanced method of adding breakpoints on the fly.

Breakpoints

Breakpoints are calls that tell the debugger to stop. You can do this in modern IDEs that are integrated into a debugger with a simple click in the sidebar. The standard way is to add binding.break at the line we want to stop at.

ruby
require 'debug' class Hornet def initialize @colors = [:yellow, :red, :black] end def show_up binding.break # debugger will stop here puts "bzzz" end end Hornet.new.show_up

By running this little program, we will get the following console output:

sh
[debug] ruby test.rb [4, 13] in test.rb 4| def initialize 5| @colors = [:yellow, :red, :black] 6| end 7| 8| def show_up => 9| binding.break # debugger will stop here 10| puts "bzzz" 11| end 12| end 13| =>#0 Hornet#show_up at test.rb:9 #1 <main> at test.rb:14 (ruby) @colors [:yellow, :red, :black] (rdgb)

As you can see, we can access the instance variable from the breakpoint.

Stepping

Let's dig into a more complex example using stepping.

ruby
class Book attr_accessor :title, :author, :price def initialize(title, author, price) @title = title @author = author @price = price end end class BookStore def initialize @books = [] end def add_book(book) @books << book end def remove_book(title) @books.delete_if { |book| book.title == title } end def find_by_title(title) @books.find { |book| book.title.include?(title) } end end # Sample Usage: store = BookStore.new book1 = Book.new("Dune", "Frank Herbert", 20.0) book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0) book3 = Book.new("Hobbit's Journey", "Unknown", 10.0) store.add_book(book1) store.add_book(book2) store.add_book(book3) puts store.find_by_title("Hobbit").title

This example app manages books in a bookstore. But at the moment, we cannot be sure which book will be returned when we search titles containing 'Hobbit'. It might well be "The Hobbit", but it's not certain.

To help debug this, we'll jump into the find_by_title method.

Let's add a breakpoint to one of the methods:

ruby
def find_by_title(title) binding.break @books.find { |book| book.title.include?(title) } end

Then launch the program and get to the breakpoint:

sh
@box [debug] ruby library.rb 20:25:07 [22, 31] in library.rb 22| def remove_book(title) 23| @books.delete_if { |book| book.title == title } 24| end 25| 26| def find_by_title(title) => 27| binding.break 28| @books.find { |book| book.title.include?(title) } 29| end 30| end 31| =>#0 BookStore#find_by_title(title="Hobbit") at library.rb:27 #1 <main> at library.rb:42 (rdbg)

The top part of the console tells us which line and file we are at. We can then query the value of the title variable.

sh
(rdbg) title "Hobbit" (rdbg)

We can run the code right in that context to see what's happening:

sh
(ruby) @books.find { |book| book.title.include?(title) } #<Book:0x00007fd05e4d59f0 @author="J.R.R. Tolkien", @price=15.0, @title="The Hobbit"> (rdbg)

Here might be a good time to reflect on how you want the program you are building and this piece of code to behave. Expressing the code through RSpec tests might be an excellent way to clarify what it should do.

Let's now continue to the next breakpoint by using the continue command.

sh
(rdbg) continue # command The Hobbit

In this case, it goes on until the end of the program.

More Commands to Assist Debugging

Of course, we can add more breakpoints to our code to stop at another place. But we can also use commands to move within the stack of our program without restarting it.

Let's add one more breakpoint to the add_book method, just after instantiating the bookstore.

ruby
def add_book(book) binding.break @books << book end # [ .. ] store = BookStore.new binding.break book1 = Book.new("Dune", "Frank Herbert", 20.0) book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0) book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)

Now, when we run the program, it will stop before the book1 variable is instantiated. The continue command will run the program until the next breakpoint or exit.

Using next

Instead of continue, we can use the next command, which will only run the next code line, so we can debug our app in smaller steps. We will need to run next twice to run the line where book1 is defined before we can inspect it.

sh
[30, 39] in library.rb 30| end 31| end 32| 33| # Sample Usage: 34| store = BookStore.new => 35| binding.break 36| book1 = Book.new("Dune", "Frank Herbert", 20.0) 37| book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0) 38| book3 = Book.new("Hobbit's Journey", "Unknown", 10.0) 39| =>#0 <main> at library.rb:35 (ruby) book1 nil (rdbg) next # command [31, 40] in library.rb 31| end 32| 33| # Sample Usage: 34| store = BookStore.new 35| binding.break => 36| book1 = Book.new("Dune", "Frank Herbert", 20.0) 37| book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0) 38| book3 = Book.new("Hobbit's Journey", "Unknown", 10.0) 39| 40| store.add_book(book1) =>#0 <main> at library.rb:36 (ruby) book1 nil (rdbg) next # command [32, 41] in library.rb 32| 33| # Sample Usage: 34| store = BookStore.new 35| binding.break 36| book1 = Book.new("Dune", "Frank Herbert", 20.0) => 37| book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0) 38| book3 = Book.new("Hobbit's Journey", "Unknown", 10.0) 39| 40| store.add_book(book1) 41| store.add_book(book2) =>#0 <main> at library.rb:37 (ruby) book1 #<Book:0x00007f50fb5f9da0 @author="Frank Herbert", @price=20.0, @title="Dune">

Each next call will run the next line. But it will not step into the code called by Book.new.

Using step

In some cases, we may know that an issue lies within a specific call. The step command is great for debugging this.

For example, when we are at line 37, we can use step to follow the execution of the Book object that fills the book2 variable.

sh
(rdbg) step # command [2, 11] in library.rb 2| 3| class Book 4| attr_accessor :title, :author, :price 5| 6| def initialize(title, author, price) => 7| @title = title 8| @author = author 9| @price = price 10| end 11| end =>#0 Book#initialize(title="The Hobbit", author="J.R.R. Tolkien", price=15.0) at library.rb:7 #1 [C] Class#new at library.rb:37 # and 1 frames (use `bt' command for all frames)

The step command brings us directly to the first line of the initialize method in the Book class. (If you are new to Ruby, the new class method is called the initialize method after it does some internal work). Now we can use the next step from within that method and follow the trail.

next and step are crucial to get familiar with, as they allow us to move forward at different levels and speeds.

Moving In the Stack

We can move up and down (or backward and forwards) in the stack by using the up and down commands. Calling up twice will get us back to line 37:

sh
[2, 11] in library.rb 2| 3| class Book 4| attr_accessor :title, :author, :price 5| 6| def initialize(title, author, price) => 7| @title = title 8| @author = author 9| @price = price 10| end 11| end =>#0 Book#initialize(title="The Hobbit", author="J.R.R. Tolkien", price=15.0) at library.rb:7 #1 [C] Class#new at library.rb:37 # and 1 frames (use `bt' command for all frames) (rdbg) up # command # No sourcefile available for library.rb =>#1 [C] Class#new at library.rb:37 (rdbg) up # command => 37| book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)

We need to call it twice as we skipped over one step, thanks to the next command: the call to the parent class of the Book class: Class itself (and the new method).

Using a Map

When we start to use up , down, next, and step, it's handy to know two more commands:

  • list: to show where we are in the code
  • bt (or backtrace): to show the trace of the steps we have followed

For example, when we are at line 37, the bt command displays the following:

sh
(rdbg) bt # backtrace command #0 Book#initialize(title="The Hobbit", author="J.R.R. Tolkien", price=15.0) at library.rb:7 #1 [C] Class#new at library.rb:37 =>#2 <main> at library.rb:37

Calling down twice brings us to step #0. We can also pass an additional integer to both up and down to move through as many steps as we want to in one go.

Knowing What's Available

A very practical command to know is ls. It will list the variables and methods available to you at your current point in the stack.

For example, on line 37, we see the following:

sh
(rdbg) ls # outline command Object.methods: inspect to_s locals: book1 book2 book3 store

Using finish

We can go to our next breakpoint using continue. However, the finish or fin command will also bring us to the next breakpoint, or to the end of our program.

You can exit more quickly with Ctrl-D or quit.

Adding Breakpoints On the Fly

A more advanced practice is to add breakpoints on the fly while running the debugger.

We have different ways to do that. Let's start with some more simple ways to add a breakpoint:

  • To a specific line — break <line number> — in the current file.
  • To the start of a specific method in a specific class: break ClassName#method_name.
sh
(rdbg) break 38 # command #0 BP - Line /mnt/data/Code/clients/AppSignal/debug/library.rb:38 (line) (rdbg) break BookStore#find_by_title # command #1 BP - Method BookStore#find_by_title at library.rb:27

Called on its own, the break command will list the existing breakpoints (the ones added through the debug console):

sh
(rdbg) break # command #0 BP - Line /mnt/data/Code/clients/AppSignal/debug/library.rb:38 (line) #1 BP - Method BookStore#find_by_title at library.rb:27

You can also remove breakpoints that are added this way using the del or delete command:

  • del will remove all breakpoints in one go (confirmation is needed).
  • del X deletes breakpoints numbered X in the breakpoints list.

Adding Conditions

You can also add conditions when setting a breakpoint. Imagine a method that goes wrong when the book title is "Germinal", but that goes ok if it's "Notre Dame". In this case, we can add a breakpoint on the method, but only if the book title matches.

sh
(rdbg) break BookStore#find_by_title if: book1.title == "Germinal" # command #1 BP - Method BookStore#find_by_title at library.rb:27 if: book1.title == "Germinal"

Integration with IDEs

Many of us rely on modern IDEs and text editors that have support for direct debugging. A good choice is rdbg: it integrates well with many IDEs.

Check the debug README for more details on rdbg.

Recap and Wrapping Up

In this post, we covered the following:

  • Installing debug
  • Adding breakpoints from your favorite code editor with binding.break
  • Looking at the value of variables and objects from a debugger session
  • Navigating within execution frames from the debugger console (with up, down, and next)
  • Listing available variables and methods at any point in the console with ls
  • Adding breakpoints and conditional breakpoints on the fly from the debugger console
  • Listing and removing breakpoints (with break, delete <number>, and delete)
  • Ending a debugging session with finish, continue, or quit.

The help command also provides plenty of details on the commands we have seen here and more. You can run help break (for example) to learn more about the break command and its subcommands.

In conclusion, the debug tool will greatly help you with debugging over the years.

Most debuggers use similar commands, so don't hesitate to try others out too (check out our post on pry-byebug, for example).

Happy coding!

P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Thomas Riboulet

Thomas Riboulet

Our guest author Thomas is a Consultant Backend and Cloud Infrastructure Engineer based in France. For over 13 years, he has worked with startups and companies to scale their teams, products, and infrastructure. He has also been published several times in France's GNU/Linux magazine and on his blog.

All articles by Thomas Riboulet

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps