Debugging in Ruby with pry-byebug
Tags: ruby
Published: 2024-05-22 09:00:22

For a software engineer, even the basic use of a debugger can save a lot of pain: adding breakpoints (places in the code the program will stop at and expose the current context) is very easy, and navigating from one breakpoint to another isn't difficult either.

And with just that, you can say goodbye to a program's many `puts` and runs. Just add one or more breakpoints and run your program. Then you're able to access not only the variables and objects you might have thought of, but also anything accessible from that point in the code.

In this article, we'll focus on `pry-byebug`, a gem that adds debugging and stack navigation to `pry` using `byebug`. We will see how to set up and use `pry-byebug`, how it integrates with Ruby programs, and a few advanced techniques.

Let's get started!

## Set Up `pry-byebug` for Ruby

The setup is really simple. Just add the [`pry-byebug` gem](https://github.com/deivid-rodriguez/pry-byebug) to your Gemfile, and then run `bundle install`. I'd advise you to add it to the `development` and `test` groups to debug tests as well.

Let's now cover some simple debugging using breakpoints.

### Basic Debugging with Breakpoints

The whole principle of debuggers is to get rid of `printf debugging` by giving you direct access to a running program's stack. So, we need to add breakpoints to tell the interpreter where to stop.

Let's take the example of a simple bookstore with software that manages the titles it offers.

```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 program is simple enough. Of course, there is no storage, but that's not needed to demonstrate what we want to do.

The last call will probably list "The Hobbit", but that's not certain and might not be the one we want. To debug this, it's best to "jump" into the `find_by_title` method.

So, add the following lines to the top of the file:

```ruby
require 'pry'
require 'pry-byebug'
```

We will also add a breakpoint to the method using `binding.pry`:

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

Let's then launch the program and get to the breakpoint:

```sh
From: test-pry-byebug/app.rb:29 BookStore#find_by_title:

    27: def find_by_title(title)
    28:   binding.pry
 => 29:   @books.find { |book| book.title.include?(title) }
    30: end

[1] pry(#<BookStore>)>
```

The first thing you should see is that we know exactly where we are: there's no need to guess which part of the program that line in STDOUT comes from. Of course, we have only one breakpoint here, but you can easily understand how useful this might be if many breakpoints are used.

We can then check the value of the `title` variable. There likely isn't much to be surprised by here. The issue is with the use of `find` (instead of `select`, or something more complex) to find and return a list of books instead of just one book.

By being right in the context, you can experiment with both `find` and `select` and decide on a course of action.

> Here might be a good point to reflect on the expected behavior of the program you are building and this piece of code. It might be good to clarify what the code should do through RSpec tests.

Once you are ok with that step or want to see what's happening until the next breakpoint, type `continue`, and the program execution will start again.

### Making Small Steps from Breakpoints

With `pry-byebug`, you can take smaller steps from breakpoints to see what happens after each line (particularly helpful for multiple calls using a method). Once you have stepped into the program and are at a breakpoint, use the `next` command to execute the next line.

Let's take the case of adding a breakpoint within the `add_book` method just before the first three books are created in the bookstore:

```ruby
def add_book(book)
  binding.pry
  @books << book
end
```

```ruby
store = BookStore.new
binding.pry
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)
```

<Banner lang="ruby" />

Upon execution, the program will first stop just before the `book1` variable is instantiated. We might want to know the result of that call. To do so, we don't have to stop the program and add another breakpoint. We just have to type `next` in the pry console and hit `Enter`.

```sh
From: test-pry-byebug/app.rb:37 :

    32: end
    33:
    34: # Sample Usage:
    35: store = BookStore.new
    36: binding.pry
 => 37: book1 = Book.new("Dune", "Frank Herbert", 20.0)
    38: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
    39: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
    40:
[1] pry(main)> next
From: test-pry-byebug/app.rb:38 :

    33:
    34: # Sample Usage:
    35: store = BookStore.new
    36: binding.pry
    37: book1 = Book.new("Dune", "Frank Herbert", 20.0)
 => 38: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
    39: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
```

We then have access to `book1`.

```sh
[1] pry(main)> book1
=> #<Book:0x00007fad37f964d8 @author="Frank Herbert", @price=20.0, @title="Dune">
```

This beats a lot of back-and-forth insertion and tweaking `puts` calls.

### Even Smaller Steps

We might want to step into the `initialize` method (called through `new`) to see more details. We can do so by using `step` instead of `next`.

```sh
From: test-pry-byebug/app.rb:8 Book#initialize:

     7: def initialize(title, author, price)
 =>  8:   @title = title
     9:   @author = author
    10:   @price = price
    11: end

[1] pry(#<Book>)> title
=> "The Hobbit"
```

This allows us to step into a method when it's called rather than skip over it as we do with `next`.

### Free from `puts`

By now, you should see how this approach saves time and frees us from using `puts` or its equivalent to debug a program. Right from the debugger shell, we can look into the values of objects and variables to figure out why a program behaves like it does. Furthermore, you can save a lot of time from the shell by adding breakpoints on the fly, thus avoiding the "quit, move `binding.pry`, restart" loop.

### Finishing a Debugging Session

You can call `continue` to go to the next breakpoint or the end of a program. `finish` executes and finishes the current step, returning just after that step. In the previous example, we end up at the following line:

```sh
[2] pry(#<Book>)> finish

From: test-pry-byebug/app.rb:39 :

    34: # Sample Usage:
    35: store = BookStore.new
    36: binding.pry
    37: book1 = Book.new("Dune", "Frank Herbert", 20.0)
    38: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
 => 39: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
```

Finally, `!!!` exits the debugger directly.

## Advanced Use Cases of `pry-byebug` for Ruby

Now let's dive into a few more advanced use cases, including rewinding and replaying, adding breakpoints on the fly, and conditional breakpoints.

### Rewinding and Replaying

Can we move back and forth between two points? Yes, totally. Given our previous case, we could move up and down from the `initialize` method to its call. Just use `up` and `down` commands after `step` to get into the method.

```sh
From: test-pry-byebug/app.rb:37 :

    34: # Sample Usage:
    35: store = BookStore.new
    36: binding.pry
 => 37: book1 = Book.new("Dune", "Frank Herbert", 20.0)
    38: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
    39: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)


[1] pry(main)> step

From: test-pry-byebug/app.rb:8 Book#initialize:

     7: def initialize(title, author, price)
 =>  8:   @title = title
     9:   @author = author
    10:   @price = price
    11: end

[1] pry(#<Book>)> up

From: test-pry-byebug/app.rb:37 :

    35: store = BookStore.new
    36: binding.pry
 => 37: book1 = Book.new("Dune", "Frank Herbert", 20.0)
    38: book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
    39: book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)


[1] pry(main)> down

From: test-pry-byebug/app.rb:8 Book#initialize:

     7: def initialize(title, author, price)
 =>  8:   @title = title
     9:   @author = author
    10:   @price = price
    11: end
```

If we need details about the stack we are in, we can call the `backtrace` method:

```sh
[1] pry(#<Book>)> backtrace
--> #0  Book.initialize(title#String, author#String, price#Float) at test-pry-byebug/app.rb:8
    ͱ-- #1  Class.new(*args) at test-pry-byebug/app.rb:37
    #2  <main> at test-pry-byebug/app.rb:37

From: /mnt/data/Code/Pier22/courses/raw-courses/rspec/git-repo/test-pry-byebug/app.rb:8 Book#initialize:

     7: def initialize(title, author, price)
 =>  8:   @title = title
     9:   @author = author
    10:   @price = price
    11: end
```

The list of frames is in the stack at the top, numbered from 0 to 2. A little cursor shows us which frame we are currently in.

## Add Breakpoints On the Fly

To avoid additional debugger exits, we can add breakpoints on the fly from the debugger console, using the `break` command.

You can add breakpoints in the current file or another file, to a specific line or the start of a specific method.

For example, let's add a breakpoint to the start of the `find_by_title` method of the `BookStore` class:

```sh
[2] pry(main)> break BookStore#find_by_title

  Breakpoint 2: BookStore#find_by_title (Enabled)

  28: def find_by_title(title)
29:   @books.find { |book| book.title.include?(title) }
30: end
```

To add it to line 38 of the current file:

```sh
[1] pry(main)> break 38

  Breakpoint 1: /mnt/data/Code/Pier22/courses/raw-courses/rspec/git-repo/test-pry-byebug/app.rb @ 38 (Enabled)

      35: binding.pry
    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)
```

A call to `break` lists all the breakpoints in place:

```sh
[4] pry(main)> break

  # Enabled At
  -------------

  1 Yes     /mnt/data/Code/Pier22/courses/raw-courses/rspec/git-repo/test-pry-byebug/app.rb @ 38
  2 Yes     BookStore#find_by_title
```

Note that they are numbered, so you can actually delete them using the `--delete` argument to `break`:

```sh
[4] pry(main)> break --delete 1
```

### Conditional Breakpoints

You might want conditional breakpoints, too.

Imagine that your code only misbehaves if a `user` object's `role` attribute is set to `:admin`.

Well, you might want to add the following breakpoint, and a condition to trigger it so that the debugger only stops if that condition is set:

```sh
[1] pry(main)> break BookStore#add_book if book.title.match /Dune/

  Breakpoint 1: BookStore#add_book (Enabled) Condition: book.title.match /Dune/
```

Using `continue`, the debugger executes the addition of `Dune` without stopping, until the next call to `add_book`.

This is a great feature that will save you a lot of time.

## Wrapping Up

By now, you should have fewer reasons to rely on `printf()` debugging when facing issues with your Ruby code. You now know how to:

- Install `pry-byebug` and some of its plugins
- Add breakpoints from your favorite code editor with `binding.pry`
- Start a debugger session (starting the app)
- Look at the value of variables and objects from the debugger session (simply calling the object or variable from the debugger console)
- Navigate within the execution frames from the debugger console (with `up`, `down`, `next`, and `frame`)
- Add breakpoints on the fly from the debugger console
- Add conditional breakpoints from the debugger console (with `break ClassName#method_name if var.nil?`, for example)
- List and remove breakpoints (with `break`, `break --delete <number>`, and `break --disable-all`)
- Conclude your debugging session with `finish`, `continue`, or `!!!`.

Finally, you can also configure `pry-byebug` through the `~/.pryrc` file to add aliases and a few custom behaviors. See the main [pry-byebug README](https://github.com/deivid-rodriguez/pry-byebug#matching-byebug-behaviour) for more details.

`pry-byebug` is a tool that will help you a lot over the years, saving you time and headaches. It's definitely worth practicing its use so that you can confidently open it the next time you're unsure how your code is behaving.

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)!**

**P.P.S. [Use AppSignal for Ruby for deeper debugging insights](https://www.appsignal.com/tour/errors).**