Ruby

Debugging in Ruby with the debug Gem

Debugging in Ruby with the debug Gem

Ruby ships with the debug gem as its built-in debugger — Ruby 3.4 bundles version 1.11. Drop binding.break where you want to stop, run your program, and an (rdbg) console opens there: next and step move line by line, continue runs to the next breakpoint, and break, catch, and watch add breakpoints on the fly.

This guide walks through each of those commands against a small program. Every transcript comes from a session on Ruby 3.4.10 with the bundled debug 1.11.0.

Debugging Without A Debugger: What's the Issue?

Many of us rely on what you might call “printf debugging”: sprinkling puts calls through the code to print the state of an object or variable, so we can tell which branches our program takes.

That approach has real costs. It means many round trips between your output and the code — you forget a puts here, leave debugging code in there, and every new question means another edit and another run. It also only answers the questions you thought to ask ahead of time, so it leans on your preconceptions about how the code runs. And if you’re going to print state anyway, print it through a logger with levels and context rather than bare puts — our guide to making the most of your logs in Rails covers that. For a bug you can reproduce locally, though, a debugger answers questions puts can’t.

A debugger inverts the workflow. You add one or more breakpoints where you want to know what’s happening, run the code, and wait for it to stop. At the breakpoint, you get a console inside the running program: you can read any variable, evaluate expressions in that context, and move through the execution one line — or one stack frame — at a time. You can even add breakpoints, including conditional ones, from inside the session, without touching the source again.

Setup

Since Ruby 3.1, a version of the debug gem ships with Ruby, and Ruby 3.4 bundles debug 1.11.0. A single require 'debug' gives any script access to binding.break — no installation needed.

To run a newer release than the bundled one (1.11.1 at the time of writing), add debug to the development and test groups of your Gemfile and run bundle install:

Ruby
group :development, :test do
  gem 'debug'
end

Grouping it with test means you can debug your tests, too.

The debug Gem at a Glance

Here are the facts most people search for, verified on Ruby 3.4.10:

FactDetail
Bundled with RubySince Ruby 3.1 (December 2021)
Version bundled with Ruby 3.4debug 1.11.0
Latest release1.11.1 on RubyGems
Breakpoint helperbinding.break, with binding.b and debugger as aliases
Console prompt(rdbg)
CLIrdbg, with remote debugging and editor integration through DAP and CDP
MaintenanceMaintained by the Ruby core team alongside Ruby releases
Repositorygithub.com/ruby/debug

Default to debug on Ruby 3.1 or newer. Its main alternative, pry-byebug, fits when you want pry’s REPL features at your breakpoints — our pry-byebug guide compares the two in detail.

Basic Debugging Techniques with Debug for Ruby

Breakpoints

Breakpoints mark the places where you want the debugger to stop. IDEs integrated with a debugger let you set one with a click in the gutter; the standard way in Ruby is a binding.break call on the line where execution should pause.

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

Running the file with ruby test.rb stops at the breakpoint and opens the debugging console:

Shell
[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]
(rdbg) continue    # command
bzzz

The (rdbg) prompt accepts both debugger commands and plain Ruby. @colors is not a command, so the console evaluates it in the current context and prints the instance variable’s value, echoing the input with a (ruby) label; recognized commands are echoed with a # command annotation instead. continue resumes the program, which prints “bzzz” and exits.

Stepping

A more involved example shows off the stepping commands — a program that manages books in a bookstore:

Ruby
require 'debug'
 
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

Which book comes back when we search for titles containing “Hobbit”? Probably “The Hobbit” — but we can’t be sure. To find out what happens inside find_by_title, add a breakpoint to it:

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

Launching the program with ruby library.rb brings us to the breakpoint:

Shell
[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) title
"Hobbit"

The header tells us which file and line we are at, and the frame line even shows the method’s argument: title="Hobbit". Querying title confirms it.

We can also run code right in that context to see what’s happening:

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

This might be a good time to reflect on how you want this piece of code to behave. Expressing the expectation through RSpec tests is an excellent way to pin down what it should do.

The continue command resumes execution; with no other breakpoints, the program runs to its end:

Shell
(rdbg) continue    # command
The Hobbit

More Commands to Assist Debugging

More breakpoints are one way to stop somewhere else. But we can also use commands to move within the program from a single stop, without restarting it.

Move the breakpoints around: one in the add_book method, and one right after the bookstore is instantiated:

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)

Running the program stops at the second breakpoint first, before book1 is instantiated:

Shell
[29, 38] in library.rb
    29|   end
    30| end
    31|
    32| # Sample Usage:
    33| store = BookStore.new
=>  34| binding.break
    35| book1 = Book.new("Dune", "Frank Herbert", 20.0)
    36| book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
    37| book3 = Book.new("Hobbit's Journey", "Unknown", 10.0)
    38|
=>#0	<main> at library.rb:34
(ruby) book1
nil

Using next

The next command runs one line of code and stops at the following one, so we can debug the app in smaller steps than continue allows. We need to run next twice to run the line where book1 is defined before we can inspect it:

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

Each next call runs one 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 made for that: it follows execution into the call.

From line 36, step follows the Book.new call that fills the book2 variable:

Shell
(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:36
  # 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 calls the initialize method after it does some internal work.) From here, next and step keep working at this deeper level, and we can follow the trail.

Get comfortable with next and step: between them, you control how deep and how fast you move.

Moving In the Stack

We can move up and down (or backward and forward) in the stack of execution frames with the up and down commands. Calling up twice from inside Book#initialize gets us back to line 36:

Shell
(rdbg) up    # command
# No sourcefile available for library.rb
=>#1	[C] Class#new at library.rb:36
(rdbg) up    # command
=>  36| book2 = Book.new("The Hobbit", "J.R.R. Tolkien", 15.0)
=>#2	<main> at library.rb:36

We need to call it twice because there is an intermediate frame between <main> and Book#initialize: the C-implemented Class#new (marked [C], with no Ruby source to display).

Both up and down also take an integer to move through several frames in one go, and the frame command jumps straight to a numbered frame:

Shell
(rdbg) frame 0    # command
    12|
=>#0	Book#initialize(title="The Hobbit", author="J.R.R. Tolkien", price=15.0) at library.rb:7

Using a Map

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

  • list: to show the source code around our current position
  • bt (or backtrace): to show the trace of the frames that lead to it

For example, when stopped inside Book#initialize, list displays the surrounding code:

Shell
(rdbg) list    # command
     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

And after moving to the <main> frame, bt shows the whole stack, marking the currently selected frame with =>:

Shell
(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:36
=>#2	<main> at library.rb:36

Calling down twice from there brings us back to frame #0.

Knowing What's Available

The ls command lists the variables and methods available at the current point in the stack. Inside Book#initialize, it shows the accessors and the method’s locals:

Shell
(rdbg) ls    # outline command
Book#methods: author  author=  price  price=  title  title=
locals: author  price  title

In the <main> frame, it lists the script’s own locals instead:

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

The info command goes one step further: it prints each local with its value, plus self. Inside Book#initialize, that’s the whole picture in one command:

Shell
(rdbg) info    # command
%self = #<Book:0x000071565d7d7a10>
title = "The Hobbit"
author = "J.R.R. Tolkien"
price = 15.0

Using finish

The finish (or fin) command is often confused with continue, but they do different things: finish runs until the current frame returns, then stops — it does not run to the next breakpoint. From our stop inside Book#initialize:

Shell
(rdbg) finish    # command
[5, 14] in library.rb
     5|
     6|   def initialize(title, author, price)
     7|     @title = title
     8|     @author = author
     9|     @price = price
=>  10|   end
    11| end
    12|
    13| class BookStore
    14|   def initialize
=>#0	Book#initialize(title="The Hobbit", author="J.R.R. Tolkien", price=15.0) at library.rb:10 #=> 15.0
  #1	[C] Class#new at library.rb:36
  # and 1 frames (use `bt' command for all frames)

The debugger stopped at the method’s end, and the frame line carries the return value: #=> 15.0. Running to the next breakpoint is continue’s job — here, it lands on the breakpoint in add_book:

Shell
(rdbg) continue    # command
[14, 23] in library.rb
    14|   def initialize
    15|     @books = []
    16|   end
    17|
    18|   def add_book(book)
=>  19|     binding.break
    20|     @books << book
    21|   end
    22|
    23|   def remove_book(title)
=>#0	BookStore#add_book(book=#<Book:0x000071565d3f91f0 @author="Frank ...) at library.rb:19
  #1	<main> at library.rb:39

To leave the session entirely, use quit (it asks for confirmation) or Ctrl-D.

Adding Breakpoints On the Fly

A more advanced practice is to add breakpoints while the debugger is running, from the console itself:

  • On a specific line of the current file: break <line number>.
  • At the start of a specific method in a specific class: break ClassName#method_name.

For this section, the bookstore keeps a single binding.break after store = BookStore.new, swaps the third book for “Germinal” by Émile Zola, and looks up two titles:

Ruby
# Sample Usage:
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("Germinal", "Émile Zola", 12.0)
 
store.add_book(book1)
store.add_book(book2)
store.add_book(book3)
 
puts store.find_by_title("Hobbit").title
puts store.find_by_title("Germinal").title

From the stop at that breakpoint, register one breakpoint by line and one by method:

Shell
(rdbg) break 42    # command
#0  BP - Line  /work/library.rb:42 (line)
(rdbg) break BookStore#find_by_title    # command
#1  BP - Method  BookStore#find_by_title at library.rb:26

Called on its own, the break command lists the breakpoints added through the console:

Shell
(rdbg) break    # command
#0  BP - Line  /work/library.rb:42 (line)
#1  BP - Method  BookStore#find_by_title at library.rb:26

You can also remove breakpoints that were added this way using the del or delete command: del alone removes all of them (after a confirmation), and del X deletes the breakpoint numbered X:

Shell
(rdbg) del 1    # delete command
deleted: #1  BP - Method  BookStore#find_by_title at library.rb:26

Two more breakpoint types are worth knowing.

The catch command stops the program where an exception of the given class is raised, before it propagates. Change the last line to search for a title the store does not have — “Dracula” — and find_by_title returns nil, so the program dies with undefined method 'title' for nil (NoMethodError). catch intercepts it at the raise site:

Shell
(rdbg) catch NoMethodError    # command
#0  BP - Catch  "NoMethodError"
(rdbg) continue    # command
The Hobbit
[38, 43] in library.rb
    38| store.add_book(book1)
    39| store.add_book(book2)
    40| store.add_book(book3)
    41|
    42| puts store.find_by_title("Hobbit").title
=>  43| puts store.find_by_title("Dracula").title
=>#0	<main> at library.rb:43
 
Stop by #0  BP - Catch  "NoMethodError"

The watch command stops when an instance variable of the current object is assigned a new value. It registers on the object whose context you are in — from a stop inside Book#initialize, watch @price triggers as soon as the value changes:

Shell
(rdbg) watch @price    # command
#0  BP - Watch  #<Book:0x00007202ecd14910> @price =
(rdbg) continue    # command
[5, 14] in library.rb
     5|
     6|   def initialize(title, author, price)
     7|     @title = title
     8|     @author = author
     9|     @price = price
=>  10|   end
    11| end
    12|
    13| class BookStore
    14|   def initialize
=>#0	Book#initialize(title="Dune", author="Frank Herbert", price=20.0) at library.rb:10 #=> 20.0
  #1	[C] Class#new at library.rb:34
  # and 1 frames (use `bt' command for all frames)
 
Stop by #0  BP - Watch  #<Book:0x00007202ecd14910> @price =  -> 20.0

One caveat: watch compares values on assignment, so mutating an object in place (@books << book) never triggers it — only reassigning the instance variable does.

Adding Conditions

You can also add conditions when setting a breakpoint. Imagine a method that goes wrong when the book title is “Germinal”, but is fine when it’s “Dune”. We can add a breakpoint on the method that fires only when the title matches. The condition is evaluated inside the method, so it can only reference variables that are in scope there — like find_by_title’s own title parameter:

Shell
(rdbg) break BookStore#find_by_title if: title == "Germinal"    # command
#0  BP - Method  BookStore#find_by_title at library.rb:26 if: title == "Germinal"
(rdbg) continue    # command
The Hobbit
[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|     @books.find { |book| book.title.include?(title) }
    28|   end
    29| end
    30|
    31| # Sample Usage:
=>#0	BookStore#find_by_title(title="Germinal") at library.rb:27
  #1	<main> at library.rb:43
 
Stop by #0  BP - Method  BookStore#find_by_title at library.rb:26 if: title == "Germinal"
(rdbg) title
"Germinal"

The first lookup (“Hobbit”) ran through without stopping — the program printed The Hobbit — and the debugger stopped on the second call, where the condition holds.

Integration with IDEs

You don’t have to start sessions from binding.break. The gem’s rdbg CLI launches a program under the debugger: rdbg library.rb stops before the first line, ready for break commands. rdbg --open starts the program as a debuggee that an editor connects to remotely — VS Code attaches through the gem’s DAP support, and Chrome DevTools through CDP (per the debug README). Any editor that speaks DAP gets gutter breakpoints, stepping, and variable inspection backed by the same commands covered in this post.

Check the debug README for more details on rdbg.

Recap and Wrapping Up

The help command provides plenty of detail on every command covered here and more; help break (for example) documents every form the break command takes.

debug Command Reference

Every example in this table ran in a session against the bookstore program:

CommandWhat it doesVerified example
breakSets a breakpoint on a line or method; alone, lists breakpointsbreak 42, break BookStore#find_by_title
stepRuns one line, stepping into method callsstep on book2 = Book.new(...) stops at Book#initialize’s first line
nextRuns one line, stepping over method callsnext on book1 = Book.new(...) stops at the next line, not inside
continueResumes until the next breakpoint, or the end of the programcontinue from Book#initialize runs to the add_book breakpoint
finishRuns until the current frame returnsfinish in Book#initialize stops at its end with #=> 15.0
frameJumps straight to frame Nframe 0 selects Book#initialize again
upMoves one frame toward the callerup from Book#initialize selects [C] Class#new
downMoves one frame back toward the stopdown twice from <main> returns to Book#initialize
btPrints the backtrace, marking the current frame with =>bt lists frames #0 to #2 of the Book.new call
listShows the source around the current linelist in Book#initialize prints lines 2–11
lsLists methods and locals available at the current framels in <main> prints locals: book1 book2 book3 store
infoPrints self and each local with its valueinfo in Book#initialize prints title = "The Hobbit"
catchStops where an exception of the given class is raisedcatch NoMethodError stops on the nil title lookup
delDeletes breakpoint N; alone, deletes all after confirmationdel 1 removes the find_by_title breakpoint

A debugger needs a console; production rarely gives you one. AppSignal’s Ruby error tracking captures the full backtrace and request context when an exception ships, so you know exactly where to put your first binding.break when you reproduce it locally.

Most debuggers use similar commands, so 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!

Frequently asked questions

What is the difference between next and step in the debug gem?
Both move one line at a time. next runs the current line and stops at the following line in the same frame, stepping over any method calls. step follows execution into a call, stopping at the first line of the method that was invoked.
How do I set a conditional breakpoint with the debug gem?
Append an if: clause when you register the breakpoint, such as break BookStore#find_by_title if: title == "Germinal". The condition must use variables that are in scope inside that method, and the debugger stops only when it evaluates to true.
What version of the debug gem ships with Ruby?
The debug gem has been bundled with Ruby since version 3.1, released in December 2021. Ruby 3.4 bundles debug 1.11.0, and the latest release on RubyGems is 1.11.1. Add the gem to your Gemfile to run a newer version than the bundled one.
Should I use the debug gem or pry-byebug?
Default to the debug gem on Ruby 3.1 or newer: it ships with Ruby, the Ruby core team maintains it, and it supports remote debugging through rdbg and VS Code. Choose pry-byebug when you want pry’s REPL features at your breakpoints.

Published , Updated

Wondering what you can do next?

  • Share this article on social media
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 install

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