
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:
group :development, :test do
gem 'debug'
endGrouping 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:
| Fact | Detail |
|---|---|
| Bundled with Ruby | Since Ruby 3.1 (December 2021) |
| Version bundled with Ruby 3.4 | debug 1.11.0 |
| Latest release | 1.11.1 on RubyGems |
| Breakpoint helper | binding.break, with binding.b and debugger as aliases |
| Console prompt | (rdbg) |
| CLI | rdbg, with remote debugging and editor integration through DAP and CDP |
| Maintenance | Maintained by the Ruby core team alongside Ruby releases |
| Repository | github.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.
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_upRunning the file with ruby test.rb stops at the breakpoint and opens the debugging console:
[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
bzzzThe (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:
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").titleWhich 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:
def find_by_title(title)
binding.break
@books.find { |book| book.title.include?(title) }
endLaunching the program with ruby library.rb brings us to the breakpoint:
[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:
(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:
(rdbg) continue # command
The HobbitMore 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:
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:
[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
nilUsing 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:
(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:
(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:
(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:36We 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:
(rdbg) frame 0 # command
12|
=>#0 Book#initialize(title="The Hobbit", author="J.R.R. Tolkien", price=15.0) at library.rb:7Using 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 positionbt(orbacktrace): to show the trace of the frames that lead to it
For example, when stopped inside Book#initialize, list displays the surrounding code:
(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| endAnd after moving to the <main> frame, bt shows the whole stack, marking the currently selected frame with =>:
(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:36Calling 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:
(rdbg) ls # outline command
Book#methods: author author= price price= title title=
locals: author price titleIn the <main> frame, it lists the script’s own locals instead:
(rdbg) ls # outline command
Object.methods: inspect to_s
locals: book1 book2 book3 storeThe 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:
(rdbg) info # command
%self = #<Book:0x000071565d7d7a10>
title = "The Hobbit"
author = "J.R.R. Tolkien"
price = 15.0Using 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:
(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:
(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:39To 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:
# 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").titleFrom the stop at that breakpoint, register one breakpoint by line and one by method:
(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:26Called on its own, the break command lists the breakpoints added through the console:
(rdbg) break # command
#0 BP - Line /work/library.rb:42 (line)
#1 BP - Method BookStore#find_by_title at library.rb:26You 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:
(rdbg) del 1 # delete command
deleted: #1 BP - Method BookStore#find_by_title at library.rb:26Two 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:
(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:
(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.0One 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:
(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:
| Command | What it does | Verified example |
|---|---|---|
break | Sets a breakpoint on a line or method; alone, lists breakpoints | break 42, break BookStore#find_by_title |
step | Runs one line, stepping into method calls | step on book2 = Book.new(...) stops at Book#initialize’s first line |
next | Runs one line, stepping over method calls | next on book1 = Book.new(...) stops at the next line, not inside |
continue | Resumes until the next breakpoint, or the end of the program | continue from Book#initialize runs to the add_book breakpoint |
finish | Runs until the current frame returns | finish in Book#initialize stops at its end with #=> 15.0 |
frame | Jumps straight to frame N | frame 0 selects Book#initialize again |
up | Moves one frame toward the caller | up from Book#initialize selects [C] Class#new |
down | Moves one frame back toward the stop | down twice from <main> returns to Book#initialize |
bt | Prints the backtrace, marking the current frame with => | bt lists frames #0 to #2 of the Book.new call |
list | Shows the source around the current line | list in Book#initialize prints lines 2–11 |
ls | Lists methods and locals available at the current frame | ls in <main> prints locals: book1 book2 book3 store |
info | Prints self and each local with its value | info in Book#initialize prints title = "The Hobbit" |
catch | Stops where an exception of the given class is raised | catch NoMethodError stops on the nil title lookup |
del | Deletes breakpoint N; alone, deletes all after confirmation | del 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?
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media

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 RibouletBecome our next author!
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!


