ruby

Debugging in Ruby with pry-byebug

Thomas Riboulet

Thomas Riboulet on

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.

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:

Shell
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)

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.

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

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

Shell
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:

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

Shell
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:

Shell
[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:

Shell
[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:

Shell
[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:

Shell
[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:

Shell
[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:

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

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