Debugging in Ruby with Debug
Tags: ruby
Published: 2024-07-17 10:42:46

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](https://www.ruby-lang.org/en/news/2021/12/25/ruby-3-1-0-released/), 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`](https://github.com/ruby/debug#use-rdbg-with-commands-written-in-ruby).

## 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](https://blog.appsignal.com/2024/05/08/debugging-in-ruby-with-pry-byebug.html), 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](https://blog.appsignal.com/ruby-magic)!**