Debugging Ruby with the debug Gem
The debug gem is Ruby’s official debugging solution for debugging Ruby code. It replaces the older lib/debug.rb standard library and gives you a fast, non-intrusive way to inspect your running code. One of its biggest advantages is that it adds zero performance overhead when you are not actively debugging.
tl;dr
Use binding.break when you are already in the source file and want to stop on one line. Use rdbg when you want to inspect a script or running process without changing the code first. Both paths land you in the same console, so the real choice is how much setup you want before the breakpoint fires.
Requirements
You need Ruby MRI 2.7 or later. Ruby 3.1 and later ship with the debug gem as a bundled gem, so no separate installation is required for those versions. If you are on Ruby 2.6 or earlier, this gem will not work. Consider using byebug or pry-byebug instead.
Installation
For Ruby versions below 3.1, install the gem manually:
gem install debug
Or add it to your Gemfile:
gem "debug", ">= 1.0.0"
Always specify version 1.0.0 or higher. The debug gem below version 1.0.0 is an older, completely different gem that is no longer maintained.
two ways to debug
source modification mode
Add require 'debug' at the top of your file and place binding.break where you want execution to pause. The debugger stops at that line and opens an interactive console where you can inspect variables and step through code:
require 'debug'
def factorial(n)
return 1 if n <= 1
n * factorial(n - 1)
end
binding.break # execution stops here
result = factorial(5)
p result
# => 120
That is the quickest workflow when the bug is already in a file you are comfortable editing. It keeps the debugger close to the code that needs attention and makes the stopping point obvious to future readers. The breakpoint is part of the source, so anyone who reads the file can see exactly where the investigation starts. binding.break has two aliases, binding.b and debugger, and all three do exactly the same thing.
CLI Mode with rdbg
If you prefer not to modify your source code, use the rdbg command-line tool:
rdbg target.rb
This starts your script under the debugger immediately, pausing at the first line. You can also attach rdbg to a running process:
rdbg --attach -- rake test
Or open your script in Chrome DevTools for a visual debugging experience with a browser-based interface:
rdbg --open target.rb
This mode is often the better choice when the code path is fragile or when you do not want to add temporary breakpoints to source control. It also works well for scripts that you only need to inspect once. The --open flag launches both the debugger server and a Chrome DevTools window, which gives you breakpoints, variable inspection, and a call stack view without leaving the browser.
debug console commands
Once the debugger stops, you get an (rdbg) prompt. Here are the most useful commands:
| Command | Short | Description |
|---|---|---|
break | b | Set a breakpoint |
step | s | Step into a method |
next | n | Step over the current line |
continue | c | Resume normal execution |
info | Show local variables and current frame | |
p <expr> | Evaluate and print an expression | |
catch | Break when an exception is raised | |
trace | Trace method calls | |
config | Show or change debugger settings |
A typical session moves through three steps: inspect the current frame, evaluate an expression, then step to the next line:
(rdbg) info locals
=>#0 factorial(n=5) at target.rb:5
%self => main
n => 5
(rdbg) p n * 10
=>50
(rdbg) next
Use info to see what variables are available in the current scope, then use p to evaluate expressions with those variables. The console is small on purpose. A short list of commands keeps the debugger easy to learn, and the repetition across sessions helps you build muscle memory quickly. Most debugging sessions follow this same rhythm: stop, inspect, adjust, and continue.
Conditional Breakpoints
You can make a breakpoint conditional by passing a Ruby expression to binding.break:
require 'debug'
counter = 0
10.times do |i|
binding.break if i == 7 # stop only when i equals 7
counter += 1
end
p counter
# => 10
Execution pauses only when the condition evaluates to true. This saves you from manually continuing through unwanted stops.
Key Methods
The main API you will use is binding.break, available in three forms:
binding.break # unconditional breakpoint
binding.b # alias
debugger # alias
binding.break if condition # conditional
You can also inspect the source location of any binding using Binding#source_location, which is part of Ruby’s core API available since Ruby 2.5. This returns a two-element array containing the file path and line number, which is helpful when you need to log where a certain code path was triggered or when you are building debugging tooling that needs to report locations programmatically:
loc = binding.source_location
p loc # => ["target.rb", 5]
rdbg command options
The rdbg command has several useful flags:
rdbg -n target.rb: run in non-stop mode, meaning the debugger starts but does not pause at the first linerdbg --stop target.rb: stop at the very first line before any code executesrdbg --attach target.rb: attach to an already running Ruby processrdbg --open target.rb: open in Chrome DevTools for a visual debugging experiencerdbg --port 12345 target.rb: start a debug server on a specific port for remote debugging
Remote Debugging
You can debug Ruby processes running on other machines or in containers by starting a debug server. The --port flag opens a TCP/IP server that external debugger frontends can connect to, which is useful when the code runs inside a Docker container, on a staging server, or on a different machine altogether. Once the server is listening, you can connect from VSCode or Chrome DevTools as if the process were local:
rdbg --port 12345 target.rb
You can then connect to it from VSCode using the vscode-rdbg extension, or from Chrome DevTools by pointing your browser to the same port. UNIX domain sockets are also supported if you prefer local connections without network exposure. That gives you enough flexibility to debug local scripts, containers, and remote services with the same tool. The transport changes, but the workflow stays familiar.
frontend integrations
The debug gem supports multiple frontends:
| Frontend | Connection Type | Extra Requirements |
|---|---|---|
| Console (rdbg) | UDS or TCP | None |
| VSCode | UDS or TCP | vscode-rdbg extension |
| Chrome DevTools | TCP/IP | Chrome browser |
The built-in console mode works out of the box. For VSCode, install the vscode-rdbg extension and configure a launch.json that points to your running debug server.
Frontends are mostly about comfort. Some developers prefer the plain terminal, while others like a GUI that shows stack frames and variables side by side. The gem supports both without changing the debugging model underneath.
common gotchas
A few things that trip people up:
Wrong gem version. Always specify >= 1.0.0 in your Gemfile. Older versions are completely different software.
JRuby is not supported. The debug gem is MRI-only. On JRuby, use ruby-debug (a separate gem) instead, which provides equivalent functionality for that runtime.
Ractor support is incomplete. If you are working with Ruby’s experimental concurrency features, be aware that the debugger may not handle all cases correctly.
Thread debugging has edge cases. Most thread scenarios work, but you may encounter quirks in complex threading situations.
require 'debug' placement matters. The debug gem must be required before any code you want to debug. If you require it after your problematic code, that code will not be instrumented.
SIGINT suspends the debugger. Pressing Ctrl-C while the debugger is running suspends execution and drops you into the debug console at the nearest safe point. This is usually helpful, but be aware it can interrupt long-running operations.
Picking a debugging workflow
The easiest way to use the debug gem is to pick one workflow and stay consistent. If you are already editing the code and want to stop at one exact line, source modification mode is usually the quickest choice. If you want to inspect a program without touching the file, rdbg is a better fit. Both modes lead to the same console, so the real decision is about how much control you want before the debugger starts.
When you are chasing a bug that only appears in one code path, a conditional breakpoint is often the calmest option. It lets the program run normally until the moment the condition turns true. That means you do not have to step through every earlier iteration by hand, which saves time and keeps the debugging session focused.
For local scripts, the simplest habit is to place binding.break near the part you want to inspect and then use info and p to look around. For application code, rdbg --open or rdbg --attach can be more comfortable because they let you keep the source unchanged while you inspect the running process. That is especially useful when the bug is in a path you do not want to edit just to debug it.
The debugger is at its most useful when you keep the session small. Set one breakpoint, inspect the variables that matter, and move on. If you find yourself adding too many breaks or trying to inspect too much state at once, it is often a sign that the code wants a smaller function or a clearer boundary.
In practice, the gem is most valuable because it lets you answer one question at a time. That is often all you need to turn an opaque failure into a concrete fix.
quick example
When you want a compact debugging loop, start the program with rdbg, stop on the line that matters, and inspect the current frame. The same pattern also works well when you want to jump from the breakpoint into the Binding reference for the methods behind the scenes.
require "debug"
def parse_total(text)
binding.break if text.nil?
Integer(text)
end
parse_total("42")
That tiny example shows the basic rhythm: stop, inspect, adjust, and continue. If the breakpoint lands where you expect, the rest of the session becomes a quick conversation with the code instead of a guessing game.
See Also
- Kernel Methods — learn about
Kernel#bindingand related methods that power the debug gem - Ruby Keywords — understand reserved keywords that appear in conditional breakpoints and debugger expressions