DEV Community

Cover image for Debugging in Ruby with pry-byebug
Thomas Riboulet for AppSignal

Posted on • Originally published at blog.appsignal.com

Debugging in Ruby with pry-byebug

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 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.

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
Enter fullscreen mode Exit fullscreen mode

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:

require 'pry'
require 'pry-byebug'
Enter fullscreen mode Exit fullscreen mode

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

def find_by_title(title)
  binding.pry
  @books.find { |book| book.title.include?(title) }
end
Enter fullscreen mode Exit fullscreen mode

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

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>)>
Enter fullscreen mode Exit fullscreen mode

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:

def add_book(book)
  binding.pry
  @books << book
end
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode

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.

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)
Enter fullscreen mode Exit fullscreen mode

We then have access to book1.

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

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.

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"
Enter fullscreen mode Exit fullscreen mode

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:

[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)
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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

[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
Enter fullscreen mode Exit fullscreen mode

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:

[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
Enter fullscreen mode Exit fullscreen mode

To add it to line 38 of the current file:

[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)
Enter fullscreen mode Exit fullscreen mode

A call to break lists all the breakpoints in place:

[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
Enter fullscreen mode Exit fullscreen mode

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

[4] pry(main)> break --delete 1
Enter fullscreen mode Exit fullscreen mode

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:

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

  Breakpoint 1: BookStore#add_book (Enabled) Condition: book.title.match /Dune/
Enter fullscreen mode Exit fullscreen mode

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

P.P.S. Use AppSignal for Ruby for deeper debugging insights.

Top comments (0)