IRB and Debugging

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18), which ships IRB 1.16.0, the debug gem 1.11.1, error_highlight 0.7.x, did_you_mean 2.0.0 and syntax_suggest 2.0.3. A Ruby program rarely does what you expect the first time, and Ruby gives you four distinct ways to find out why, which this page covers in the order you reach for them:

  • Ask the language in a REPL: IRB lets you load your code, call it, inspect the result, read the source and the documentation of any method, and, with binding.irb, do all of that from inside a running program.

  • Stop the program and walk through it with the debug gem (rdbg, binding.break): breakpoints, stepping, frame navigation, exception breakpoints, watchpoints, tracing, replay, remote attach and an editor integration.

  • Print what you need to see with p, pp, inspect and a few small idioms, which is still the fastest tool for most bugs.

  • Read what the interpreter already tells you: the error message, its source snippet (error_highlight), its spelling suggestion (did_you_mean) and its syntax diagnosis (syntax_suggest).

flowchart TD A[A bug or a surprise] --> B{Do you know roughly where?} B -- no --> C[Read the message: error_highlight and did_you_mean] C --> D{Still unclear?} D -- yes --> E[Reproduce it in IRB: load the code and call it] B -- yes --> F{Can you see the values you need?} F -- "yes, print them" --> G[p, pp, warn, tap] F -- "need to look around" --> H{Can you edit the code?} H -- yes --> I[binding.irb or binding.break] H -- no --> J[rdbg script.rb or rdbg -c command] E --> I G --> K[Fixed?] I --> K J --> K

The page builds on Getting Started (installing Ruby and running scripts), Reflection, Introspection and Tracing (the Binding, caller and TracePoint machinery that every tool here is built on), Exceptions and Error Handling (raise, backtrace, detailed_message) and RubyGems and Bundler (why a gem that is installed can still fail to load under bundle exec). Unit tests are the other half of debugging; see Testing with Minitest and RSpec and Test Doubles. Profilers and benchmarks are in Performance, Benchmarking and Profiling, and the ruby flags that this page mentions (-w, -r, -I, --disable) are explained in Ruby Command Line and Environment.

Every example was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using the configuration of the running shelf gem (double-quoted strings, NewCops: enable, Style/Documentation off). The two scripts that start an interactive session on purpose (binding.irb and binding.break) carry a rubocop:disable Lint/Debugger comment, because that cop exists to keep such calls out of committed code. The interactive transcripts are real sessions copied from a terminal, trimmed to the relevant lines; absolute paths are replaced by /path/to/shelf and memory addresses are the ones the session printed. The examples that need the gem are run with ruby -I/path/to/shelf/lib, so require "shelf" finds it (see the shelf gem source).

The tools at a glance

Tool What it is Where it comes from in Ruby 4.0

irb

An interactive Ruby shell with multi-line editing, colour, completion, documentation lookup and binding.irb

Bundled gem since 4.0; a default gem up to 3.4. Under Bundler it must be in the Gemfile

rdbg, debug

The debugger: breakpoints, stepping, remote attach, DAP for editors

Bundled gem (present in 3.2, 3.3, 3.4 and 4.0). Under Bundler it must be in the Gemfile

p, pp, inspect, pretty_inspect

Printing a value for a developer

Core (p) and the pp library, which Kernel#pp loads on first use

error_highlight

Adds the failing expression and a caret line to error messages

Default gem, loaded by default

did_you_mean

Adds spelling suggestions to error messages

Default gem, loaded by default

syntax_suggest

Explains end and bracket mismatches in a file that does not parse

Default gem since 3.2, loaded by default

repl_type_completor

Type-aware completion for IRB

Bundled gem since 3.4; the default completor of IRB when it is available

IRB

IRB (*I*nteractive *R*u*B*y) reads an expression, evaluates it, prints the result with inspect and loops. It is what you get from the irb command, and the same engine runs inside a program when you call binding.irb. Its documentation lives with the gem, at https://ruby.github.io/irb/ and in irb --help; since IRB is a bundled gem in 4.0, the core documentation at docs.ruby-lang.org/en/4.0 no longer has an IRB page.

Starting IRB

$ irb                                # an empty session
$ irb -I lib -r shelf                # put lib on the load path and require the gem first
$ irb -w                             # same as ruby -w: show warnings
$ irb -f                             # do not read any .irbrc
$ irb --nocolorize --noautocomplete  # plain output, useful in logs and CI consoles

The options in irb --help are the same as those of ruby where they overlap (-d, -r, -I, -U, -E, -w, -W) plus the IRB-specific ones in the table below. All of them can also be set from a configuration file (see The .irbrc file).

Option Effect

-f

Do not load any configuration file

-r library, -I path

require a library / add a directory to $LOAD_PATH before the first prompt, as in ruby

--echo, --noecho

Print (default) or do not print the value of each expression

--echo-on-assignment, --noecho-on-assignment, --truncate-echo-on-assignment

Print, hide or shorten the value of an assignment. The default is truncated, which is why a large x = ... ends in ...

--inspect, --noinspect

Print the result with inspect (default) or with to_s

--multiline, --singleline, --nomultiline, --nosingleline

Choose the line editor: the multi-line Reline editor (default), the single-line one, or none

--colorize, --nocolorize

Syntax and result colours (default on; the NO_COLOR environment variable turns them off)

--autocomplete, --noautocomplete

The completion dialog that appears while you type (default on)

--regexp-completor, --type-completor

Choose how completion candidates are computed (see Type-based completion)

--no-pager

Print long output (such as show_doc) directly instead of through a pager

--prompt mode, --simple-prompt, --noprompt, --inf-ruby-mode

Choose the prompt (default, classic, simple, inf-ruby, xmp, null)

--context-mode n

How the binding of a new workspace is created (0 to 5)

--script, --noscript

Treat the first argument as a script (default) or leave all arguments in ARGV

--back-trace-limit n, --tracer

Number of backtrace lines kept at each end of an error, and a stack trace for every command

--single-irb

Share self between a session and its child sessions

--extra-doc-dir dir

Add an RI documentation directory to the documentation dialog

--verbose, --noverbose

More or less detail about what IRB does

A first session

At the prompt, every complete expression is evaluated and its result printed after =>. A statement that is not yet complete (an open do, a trailing operator, an unclosed bracket) makes the editor continue on the next line, and the editor indents it for you. The result of the last expression is available as _, which is the quickest way to keep working on a value you forgot to assign.

irb(main):001> isbn = Shelf::Isbn.new("978-0-596-51774-8")
=> #<data Shelf::Isbn digits="9780596517748">
irb(main):002> isbn.digits.length
=> 13
irb(main):003> _ + 1
=> 14

An assignment prints its value too, but truncated when it is long (the default --truncate-echo-on-assignment), so that loading a large object into a variable does not flood the terminal:

irb(main):002> book = Shelf::Book.new(isbn:, title: "The Ruby Programming Language", author: Shelf::Author.new("Flanagan"))
=>
#<data Shelf::Book
...

Evaluating the variable on its own prints the whole value, laid out with pp (see pp and p). Two details are worth remembering. The value printed after => is the return value of the expression, not what the expression printed: puts "x" shows x and then => nil. And in Ruby 3.4 and later Hash#inspect prints {isbn: 1} and {"a" => 1}, so the output you see in IRB matches what p prints in a script of the same version.

Commands

IRB has two kinds of input. Anything that is valid Ruby is evaluated. A command is a name followed by free text that does not have to be valid Ruby, such as ls -g pattern or show_source Shelf::Isbn#to_s. A command is recognised only when it is the first word of the input and no local variable of that name exists: if you assign ls = 3, the word ls means the variable until you leave it. Type help for the list in your version and help command for one command.

Command What it does

help [command]

List the commands, or describe one

ls [obj] [-g pattern]

Show the methods, constants, instance variables and local variables of an object or the current context, grouped by the class that defines them; -g filters by a pattern

show_source name, $ name

Print the source code of a method, class, module or constant (Shelf::Isbn#to_s, Shelf::Isbn)

show_doc [name]

Look the name up in the RI documentation and show it; with no argument start an interactive ri session

whereami, @

Show the source around the line where binding.irb stopped

cd obj, cd ..

Make obj the receiver (self) of the session, or go back; the prompt shows it

chws obj, pushws obj, popws, workspaces, cwws

Change, push, pop, list and show the current workspace (the same idea as cd, with a stack)

history [-g pattern]

The input history, optionally filtered; history -G pattern also exists

measure, measure :off

Print the wall time of each expression (measure :stackprof profiles with the stackprof gem)

edit [file or name]

Open a file or the definition of a method in $VISUAL or $EDITOR

copy expr

Copy the printed result to the clipboard (program overridable with IRB_COPY_COMMAND)

context, irb_info

The current configuration, and the Ruby, IRB, input method and completion in use

irb_load file, irb_require lib, source file

Load or require a file inside the session. source evaluates the file in the current workspace

exit, quit, exit!, disable_irb

Leave the session (a binding.irb session lets the program continue), leave the whole process, or turn later binding.irb calls into no-ops

debug, break, catch, next, step, continue, finish, delete, backtrace, info

Hand control to the debug gem (see Using IRB and the debugger together)

irb, jobs, fg, kill

Child sessions (deprecated in the IRB version shipped with 4.0)

A session that uses the most useful ones, against the shelf gem:

irb(main):004> ls isbn
Shelf::Isbn#methods: digits  to_s
Data#methods:
  ==                  deconstruct  deconstruct_keys  eql?  hash  inspect  members  pretty_print
  pretty_print_cycle  to_h         with

ls groups methods by the class or module that owns them, from the most specific to the most general, which is the method lookup order. Here digits and to_s come from Shelf::Isbn and the rest from Data. show_source answers "where is this defined, and how":

history lists the input numbered from 0, and history -g isbn keeps the lines that match:

irb(main):005> history
4: history
3: ls isbn
2: _ + 1
1: isbn.digits.length
0: isbn = Shelf::Isbn.new("978-0-596-51774-8")
irb(main):006> history -g isbn
5: history -g isbn
3: ls isbn
1: isbn.digits.length
0: isbn = Shelf::Isbn.new("978-0-596-51774-8")
irb(main):008> show_source Shelf::Isbn#to_s

From: /path/to/shelf/lib/shelf/isbn.rb:13

    def to_s = digits

show_source and $ print the same thing, and the first line gives the file and line, which edit can open. Documentation comes from the RI database, which is installed with Ruby for the core and standard library (a gem has its own only if its documentation was installed or if you add its directory with --extra-doc-dir, so show_doc Shelf::Isbn answers "Nothing known about Shelf::Isbn"):

irb(main):008> show_doc Array#sum
== Array#sum

(from ruby core)
------------------------------------------------------------------------

Source: https://ruby.github.io/irb/#label-Commands[IRB documentation: Commands]

  sum(init = 0) -> object
  sum(init = 0) {|element| ... } -> object
------------------------------------------------------------------------

With no block given, returns the sum of init and all elements of self;
...

cd is a quick way to work inside an object, with its instance variables and private methods in reach (the same idea as the instance_eval of Hooks, eval and DSLs, with a prompt):

irb(main):009> cd isbn
irb(9780596517748):010> digits
=> "9780596517748"
irb(9780596517748):011> self
=> #<data Shelf::Isbn digits="9780596517748">
irb(9780596517748):012> cd ..
irb(main):013> measure
TIME is added.
irb(main):014> (1..200_000).sum { |n| n * n }
processing time: 0.004844s
=> 2666686666700000
irb(main):015> measure :off

Pitfalls in the REPL

  • The history is global. Every session appends to the same history file, by default ~/.irb_history, which is why the history numbers in a long-lived shell are large. IRB.conf[:SAVE_HISTORY] (default 1000 lines) and IRB.conf[:HISTORY_FILE] control it, and a secret typed at the prompt stays in the file.

  • Redefinition is silent. Reloading a class by pasting it again adds methods to the existing class instead of replacing it, exactly as in a file. Restart the session, or use load "file.rb", when stale methods matter.

  • _ is overwritten by every expression, including the ones IRB evaluates for you, so assign anything you need to keep.

  • A command is shadowed by a local variable. After history = [] the word history is the array. Use binding.local_variables to see what you have defined.

  • Output is inspect, not to_s. A Data or Struct whose to_s you overrode still prints with its structure in IRB.

  • Ctrl-C interrupts the running expression, not the shell (the default IRB.conf[:IGNORE_SIGINT] is true), and Ctrl-D at an empty prompt exits.

binding.irb

binding.irb starts an IRB session inside the running program, with the local variables, self and the current method of the line where it is written. It is Binding#irb, so you can call it on any binding, and it is the Ruby counterpart of debugger in other languages.

# frozen_string_literal: true

require "shelf"

isbn = Shelf::Isbn.new("978-0-596-51774-8")
author = Shelf::Author.new("Flanagan")
book = Shelf::Book.new(isbn:, title: "The Ruby Programming Language", author:)
catalog = Shelf::Catalog.new.add(book)

titles = catalog.map(&:title)
binding.irb # rubocop:disable Lint/Debugger

puts titles.size

Running the script with ruby -I/path/to/shelf/lib e01_inspect_loan.rb stops at line 11 and shows the surrounding code with an arrow. Everything you type is evaluated in the script’s scope, so the local variables are there, and so is any change you make:

From: e01_inspect_loan.rb @ line 11 :

     6: author = Shelf::Author.new("Flanagan")
     7: book = Shelf::Book.new(isbn:, title: "The Ruby Programming Language", author:)
     8: catalog = Shelf::Catalog.new.add(book)
     9:
    10: titles = catalog.map(&:title)
 => 11: binding.irb # rubocop:disable Lint/Debugger
    12:
    13: puts titles.size

irb(main):001> titles
=> ["The Ruby Programming Language"]
irb(main):002> catalog.first.author.name
=> "Flanagan"
irb(main):003> title = catalog.first.title.upcase
=> "THE RUBY PROGRAMMING LANGUAGE"
irb(main):004> catalog.first.title = "x"
e01_inspect_loan.rb(irb):4:in '<main>': undefined method 'title=' for an instance of Shelf::Book (NoMethodError)
	from e01_inspect_loan.rb:11:in 'Binding#irb'
	from e01_inspect_loan.rb:11:in '<main>'
irb(main):005> exit
1

The error in the last command is the point: an exception raised by what you type is printed and the session continues, so trying things out cannot end the program. Typing exit (or Ctrl-D) resumes the program after the binding.irb line, which is why 1 was printed by the puts after the session. exit! ends the whole process, and disable_irb makes every later binding.irb call do nothing, which is useful when a call sits in a loop.

Where binding.irb is useful and where it is not:

  • It is for a developer at a terminal. A program without a terminal on standard input (a service, a job, a test run with piped input) has nothing for IRB to read; use the remote mode of the debugger instead (see Remote debugging).

  • Put it where the interesting values are in scope, including inside a rescue clause (rescue => e then binding.irb), inside a block, or in a method called from a test. binding.irb in a test run waits forever in CI, so remove it before committing; RuboCop’s Lint/Debugger cop exists to catch it.

  • Under Bundler it needs irb in the Gemfile (see Debugging under Bundler).

  • From IRB you can reach the debugger without restarting (see Using IRB and the debugger together).

The .irbrc file

IRB reads Ruby code from a configuration file at startup (unless -f is given). The first file found in this order is used:

  1. the path in the IRBRC environment variable;

  2. $XDG_CONFIG_HOME/irb/irbrc;

  3. ~/.irbrc;

  4. ~/.config/irb/irbrc (only when XDG_CONFIG_HOME is not set);

  5. .irbrc, _irbrc or $irbrc in the current directory.

Because the file is plain Ruby, it can configure IRB through the IRB.conf hash and define commands and helpers. Hash entries set in the file win over command-line options, and IRB.conf is read once, when the file is interpreted, so changes made later in a session do not affect the running context (use conf, the current context, for that).

# frozen_string_literal: true

# ~/.irbrc or ./.irbrc: Ruby code that runs once when IRB starts.
require "irb/command"
require "irb/helper_method"

IRB.conf[:SAVE_HISTORY] = 5_000              # lines kept in the history file
IRB.conf[:HISTORY_FILE] = File.join(Dir.home, ".irb_history")
IRB.conf[:PROMPT_MODE] = :SIMPLE             # ">> " instead of "irb(main):001> "
IRB.conf[:USE_PAGER] = false
IRB.conf[:ECHO_ON_ASSIGNMENT] = false        # do not print the value on `x = ...`
IRB.conf[:COMPLETOR] = :type                 # :type or :regexp
IRB.conf[:LOAD_MODULES] = %w[json]           # same as irb -r json (the rc file wins over -r)

# A command: it receives the raw text after its name, so the argument need not be valid Ruby.
class Isbn13Command < IRB::Command::Base
  category "Shelf"
  description "Convert an ISBN-10 to an ISBN-13"
  help_message "Usage: isbn13 <isbn-10>"

  def execute(arg)
    body = "978#{arg.delete('-')[0, 9]}"
    weighted = body.chars.each_with_index.sum { |digit, i| digit.to_i * (i.even? ? 1 : 3) }
    puts "#{body}#{(10 - (weighted % 10)) % 10}"
  end
end
IRB::Command.register(:isbn13, Isbn13Command)

# A helper method: an ordinary method call that returns a value.
sample_catalog = Class.new(IRB::HelperMethod::Base) do
  description "A catalog with one book in it"

  def execute
    require "shelf"
    isbn = Shelf::Isbn.new("978-0-596-51774-8")
    book = Shelf::Book.new(isbn:, title: "The Ruby Programming Language", author: Shelf::Author.new("Flanagan"))
    Shelf::Catalog.new.add(book)
  end
end
IRB::HelperMethod.register(:sample_catalog, sample_catalog)

Using the file above (as IRBRC=e03_irbrc.rb irb -I lib), the command and the helper behave like built-ins, and help isbn13 prints the help_message:

>> isbn13 0-596-51774-0
9780596517748
>> help isbn13
Usage: isbn13 <isbn-10>
>> c = sample_catalog
>> c.count
=> 1
>> c.first.title
=> "The Ruby Programming Language"
>> conf.prompt_mode
=> :SIMPLE
>> IRB.conf[:SAVE_HISTORY]
=> 5000
>>

The most useful IRB.conf entries and the environment variables behind them:

IRB.conf key Environment variable / option Meaning

:SAVE_HISTORY, :HISTORY_FILE

Number of lines kept (default 1000; false or 0 disables saving, a negative number keeps everything), and the file

:PROMPT_MODE, :PROMPT

--prompt

Prompt style; :PROMPT is a hash of formats where %N, %m, %03n and %i stand for the name, self, the line number and the indent level

:USE_AUTOCOMPLETE

IRB_USE_AUTOCOMPLETE=false, --noautocomplete

The automatic completion dialog

:COMPLETOR

IRB_COMPLETOR=type or regexp, --type-completor

Which completor computes candidates

:USE_COLORIZE

NO_COLOR, --nocolorize

Colour output

:USE_PAGER

--no-pager, RI_PAGER, PAGER

Send long output through a pager

:ECHO, :ECHO_ON_ASSIGNMENT

--noecho, --noecho-on-assignment

Print results; ECHO_ON_ASSIGNMENT can be true, false or :truncate

:INSPECT_MODE

--noinspect

true for inspect, false for to_s, or :pp, :yaml or :marshal

:LOAD_MODULES

-r

Libraries required at startup

:EXTRA_DOC_DIRS

--extra-doc-dir

Additional RI directories for show_doc and the documentation dialog

:BACK_TRACE_LIMIT, :USE_TRACER

--back-trace-limit, --tracer

Backtrace lines kept and per-command tracing

:AT_EXIT

Hooks called when IRB exits

:VERBOSE

--verbose

More detail about IRB itself

:COMMAND_ALIASES

A hash of aliases such as { "$": :show_source, "@": :whereami }

Other variables IRB reads are EDITOR or VISUAL (for edit), IRB_COPY_COMMAND (for copy), RI_PAGER and PAGER, and IRB_LANG, LC_MESSAGES, LC_ALL and LANG for the language of its messages.

Extending IRB: commands and helper methods

Since IRB 1.13 there is a supported API to add features, shown in the .irbrc above. Choose between the two by what the input looks like:

  • A command (IRB::Command::Base subclass registered with IRB::Command.register) receives the raw text after its name as one String, so it can accept things that are not Ruby (isbn13 0-596-51774-0, ls -g pattern). It does not return a value to the session. Use category, description and help_message to put it in help.

  • A helper method (IRB::HelperMethod::Base subclass registered with IRB::HelperMethod.register) is an ordinary method call that returns an object (sample_catalog.first). It shows up under "Helper methods" in help, next to the built-in conf.

A gem can ship its own commands and helpers, loaded from its .irbrc or from a library that the user requires, which is how framework consoles add their shortcuts. If you do not know which to pick, the guide recommends a command first, because it handles more kinds of input.

Type-based completion

IRB offers completion in two forms: pressing Tab, and the dialog that opens automatically while you type, showing candidates and, for the selected one, its documentation. How candidates are computed depends on the completor:

  • The regexp completor looks at the text before the cursor with regular expressions and may evaluate the receiver of a simple method call. It is simple and works everywhere.

  • The type completor (repl_type_completor, a bundled gem since Ruby 3.4) parses the code with Prism and infers the type of the receiver from the code and from the RBS signatures of the core classes, without running anything. After titles.map { it. it knows it is a String. It is the default in IRB on Ruby 3.4 and later when the gem is available; IRB_COMPLETOR=regexp, --regexp-completor or IRB.conf[:COMPLETOR] = :regexp switches back, and --type-completor forces it (printing a warning when the gem cannot be loaded).

irb_info shows which one is active:

irb(main):001> irb_info
Ruby version: 4.0.7
IRB version: irb 1.16.0 (2025-12-17)
InputMethod: RelineInputMethod with Reline 0.6.3
Completion: Autocomplete, ReplTypeCompletor: 0.1.12, Prism: 1.8.1, RBS: 3.10.0
RUBY_PLATFORM: arm64-darwin23
East Asian Ambiguous Width: 1

The gem has a small public API that IRB itself uses, so you can see what completion would offer without typing in a terminal. The candidates are the remaining characters of each match (after titles.first.up the completion case completes upcase):

# frozen_string_literal: true

require "repl_type_completor"
require "shelf"

# Build a binding that holds a few local variables, as an IRB session would.
session = binding
session.local_variable_set(:isbn, Shelf::Isbn.new("978-0-596-51774-8"))
session.local_variable_set(:titles, %w[Ruby Rails])
session.local_variable_set(:stock, { "Ruby" => 3 })

ReplTypeCompletor.load_rbs # IRB loads the RBS signatures in the background

["isbn.dig", "titles.first.up", "titles.map { it.str", "stock.fetch(\"Ruby\").ze"].each do |code|
  result = ReplTypeCompletor.analyze(code, binding: session)
  puts "#{code.ljust(26)} => #{result.completion_candidates.sort.inspect}"
end

Output:

isbn.dig                   => ["its"]
titles.first.up            => ["case", "case!", "to"]
titles.map { it.str        => ["ip", "ip!"]
stock.fetch("Ruby").ze     => ["ro?"]

The completor knows what Ruby knows: isbn.dig can only complete to digits, titles.first.up offers the String methods and stock.fetch("Ruby").ze offers the Integer method zero?, which a purely textual completor cannot know without running the code. Its limits are those of static inference: a value that only exists at runtime (the result of eval, of send with a computed name, of method_missing) is unknown to it, and it falls back to listing nothing rather than guessing.

Using IRB and the debugger together

The debug command in an IRB session (and break, catch, next, step, continue, finish, delete, backtrace and info, which start the debugger first and then run the same command there) switches the session to the debug gem without restarting. The prompt becomes irb:rdbg, and you keep IRB’s editor, completion and show_source while using the stepping commands of the next sections. Running the binding.irb script of the previous section with a debug command:

irb(main):001> debug
irb:rdbg(main):002> info
%self = main
isbn = #<data Shelf::Isbn digits="9780596517748">
author = #<data Shelf::Author name="Flanagan">
book = #<data Shelf::Book isbn=#<data Shelf::Isbn digits="9780596517748">, t...>
catalog = #<Shelf::Catalog:0x0000000122478358 @store=#<Shelf::Store::Memory:...>
titles = ["The Ruby Programming Language"]
irb:rdbg(main):003> next
[8, 13] in e01_inspect_loan.rb
     8| catalog = Shelf::Catalog.new.add(book)
     9|
    10| titles = catalog.map(&:title)
    11| binding.irb # rubocop:disable Lint/Debugger
    12|
=>  13| puts titles.size
=>#0	<main> at e01_inspect_loan.rb:13
irb:rdbg(main):004> continue
1

(When the same session is run with ruby -w, IRB 1.16.0 prints "method redefined" warnings for its own debugger bridge; they come from the gem, not from your code.) The reverse also works: the irb command inside an rdbg session starts IRB as the debug console, and RUBY_DEBUG_IRB_CONSOLE=true or config set irb_console 1 in ~/.rdbgrc makes it the default.

The debug gem

The debug gem is the debugger that comes with Ruby 3.1 and later. Until Ruby 3.0 the standard library had a different debug.rb; the gem was written to replace it, and it also replaces, for most people, the community debuggers that earlier tutorials describe. It is a bundled gem (installed with Ruby), is built on TracePoint, supports threads, can be driven from a terminal, a script, a remote socket, VS Code or Chrome DevTools, and is maintained at https://github.com/ruby/debug.

Three ways to start it

$ ruby -r debug target.rb         # or put `require "debug"` at the top: stops only at binding.break / debugger
$ rdbg target.rb                  # run under the debugger, stopped at the first line
$ rdbg -c -- bundle exec rake test   # run another Ruby command under the debugger
  • In the source. Add require "debug" and call binding.break (aliases binding.b and debugger) where you want to stop. The program runs normally until that line, then opens the debug console. It is binding.irb with a debugger instead of a REPL, and require "debug" is the same as rdbg --nonstop --no-sigint-hook.

  • From the command line. rdbg script.rb args starts script.rb like ruby script.rb args and stops at the first line; rdbg -n (--nonstop) does not stop there, so only your breakpoints fire. rdbg — -r foo -e code passes interpreter options after --, as in rdbg — -I/path/to/shelf/lib target.rb: rdbg has no -I of its own, and rdbg -I lib target.rb fails with "invalid option: -I".

  • For another Ruby executable. rdbg -c — bundle exec rake test runs the command with rdbg -c and the debugger attaches to the Ruby it starts. The -- separates rdbg’s options from the command’s (rdbg -c rake -T would be read as rdbg -c -T — rake). Under Bundler the Gemfile must list debug.

require "debug/start" starts a session stopped at that point like rdbg does, require "debug/open" and require "debug/open_nonstop" open the remote port (see Remote debugging), and DEBUGGER__.start(**options) from debug/session starts one with configuration given in code. They must be required before the code you want to debug is loaded.

A debugging session

The target is a small script from the shelf scenario with a lending-fee calculation:

# frozen_string_literal: true

require "shelf"

# Fee in cents: 25 per day late, capped at 1000.
def late_fee(days_late)
  fee = days_late * 25
  fee = 1000 if fee > 1000
  fee
end

def total_fees(loans)
  loans.sum do |loan, days|
    fee = loan.state == :overdue ? late_fee(days) : 0
    fee
  end
end

isbn = Shelf::Isbn.new("978-0-596-51774-8")
book = Shelf::Book.new(isbn:, title: "The Ruby Programming Language", author: Shelf::Author.new("Flanagan"))
loans = [[Shelf::Loan.new(book, state: :overdue), 12], [Shelf::Loan.new(book, state: :lent), 3]]

puts "total: #{total_fees(loans)}"

Run it with rdbg — -I/path/to/shelf/lib e04_debug_target.rb. It stops at line 3; the commands below are what was typed. A method breakpoint with a condition stops only for long overdue loans, info shows the locals, next steps over lines, display re-evaluates an expression at every stop, up and down move between frames, and delete removes a breakpoint:

[1, 10] in e04_debug_target.rb
     1| # frozen_string_literal: true
     2|
=>   3| require "shelf"
     4|
     5| # Fee in cents: 25 per day late, capped at 1000.
     6| def late_fee(days_late)
     7|   fee = days_late * 25
     8|   fee = 1000 if fee > 1000
     9|   fee
    10| end
=>#0	<main> at e04_debug_target.rb:3
(rdbg) break Object#late_fee if: days_late > 10    # command
Unknown method name: "late_fee"
#0  BP - Method (pending)  Object#late_fee if: days_late > 10
(rdbg) break    # command
#0  BP - Method (pending)  Object#late_fee if: days_late > 10
(rdbg) continue    # command
[2, 11] in e04_debug_target.rb
     2|
     3| require "shelf"
     4|
     5| # Fee in cents: 25 per day late, capped at 1000.
     6| def late_fee(days_late)
=>   7|   fee = days_late * 25
     8|   fee = 1000 if fee > 1000
     9|   fee
    10| end
    11|
=>#0	Object#late_fee(days_late=12) at e04_debug_target.rb:7
  #1	block {|loan=#<Shelf::Loan:0x0000000120f88dd0 @book=#<..., days=12|} in Object#total_fees at e04_debug_target.rb:14
  # and 3 frames (use `bt' command for all frames)

Stop by #0  BP - Method  Object#late_fee at e04_debug_target.rb:6 if: days_late > 10
(rdbg) info    # command
%self = main
days_late = 12
fee = nil
(rdbg) bt    # backtrace command
=>#0	Object#late_fee(days_late=12) at e04_debug_target.rb:7
  #1	block {|loan=#<Shelf::Loan:0x0000000120f88dd0 @book=#<..., days=12|} in Object#total_fees at e04_debug_target.rb:14
  #2	[C] Array#sum at #<none>:0
  #3	Object#total_fees(loans=[[#<Shelf::Loan:0x0000000120f88dd0 @book...) at e04_debug_target.rb:13
  #4	<main> at e04_debug_target.rb:23
(rdbg) next    # command
[3, 12] in e04_debug_target.rb
     3| require "shelf"
     4|
     5| # Fee in cents: 25 per day late, capped at 1000.
     6| def late_fee(days_late)
     7|   fee = days_late * 25
=>   8|   fee = 1000 if fee > 1000
     9|   fee
    10| end
    11|
    12| def total_fees(loans)
=>#0	Object#late_fee(days_late=12) at e04_debug_target.rb:8
  #1	block {|loan=#<Shelf::Loan:0x0000000120f88dd0 @book=#<..., days=12|} in Object#total_fees at e04_debug_target.rb:14
  # and 3 frames (use `bt' command for all frames)
(rdbg) next    # command
[4, 13] in e04_debug_target.rb
     4|
     5| # Fee in cents: 25 per day late, capped at 1000.
     6| def late_fee(days_late)
     7|   fee = days_late * 25
     8|   fee = 1000 if fee > 1000
=>   9|   fee
    10| end
    11|
    12| def total_fees(loans)
    13|   loans.sum do |loan, days|
=>#0	Object#late_fee(days_late=12) at e04_debug_target.rb:9
  #1	block {|loan=#<Shelf::Loan:0x0000000120f88dd0 @book=#<..., days=12|} in Object#total_fees at e04_debug_target.rb:14
  # and 3 frames (use `bt' command for all frames)
(rdbg) p fee    # command
=> 300
(rdbg) display fee    # command
0: fee = 300
(rdbg) next    # command
[5, 14] in e04_debug_target.rb
     5| # Fee in cents: 25 per day late, capped at 1000.
     6| def late_fee(days_late)
     7|   fee = days_late * 25
     8|   fee = 1000 if fee > 1000
     9|   fee
=>  10| end
    11|
    12| def total_fees(loans)
    13|   loans.sum do |loan, days|
    14|     fee = loan.state == :overdue ? late_fee(days) : 0
=>#0	Object#late_fee(days_late=12) at e04_debug_target.rb:10 #=> 300
  #1	block {|loan=#<Shelf::Loan:0x0000000120f88dd0 @book=#<..., days=12|} in Object#total_fees at e04_debug_target.rb:14
  # and 3 frames (use `bt' command for all frames)
0: fee = 300
(rdbg) up    # command
=>  14|     fee = loan.state == :overdue ? late_fee(days) : 0
=>#1	block {|loan=#<Shelf::Loan:0x0000000120f88dd0 @book=#<..., days=12|} in Object#total_fees at e04_debug_target.rb:14
(rdbg) info locals    # command
%self = main
loan = #<Shelf::Loan:0x0000000120f88dd0 @book=#<data Shelf::Book isbn=#<data...>
days = 12
fee = nil
loans = [[#<Shelf::Loan:0x0000000120f88dd0 @book=#<data Shelf::Book isbn=#<da...
(rdbg) ls loan    # outline command
Shelf::Loan#methods: book  describe  state
instance variables: @book  @state
locals: days  fee  loan  loans
(rdbg) down    # command
=>  10| end
=>#0	Object#late_fee(days_late=12) at e04_debug_target.rb:10 #=> 300
(rdbg) delete 0    # command
deleted: #0  BP - Method  Object#late_fee at e04_debug_target.rb:6 if: days_late > 10
(rdbg) finish    # command
[11, 20] in e04_debug_target.rb
    11|
    12| def total_fees(loans)
    13|   loans.sum do |loan, days|
    14|     fee = loan.state == :overdue ? late_fee(days) : 0
    15|     fee
=>  16|   end
    17| end
    18|
    19| isbn = Shelf::Isbn.new("978-0-596-51774-8")
    20| book = Shelf::Book.new(isbn:, title: "The Ruby Programming Language", author: Shelf::Author.new("Flanagan"))
=>#0	block {|loan=#<Shelf::Loan:0x0000000120f88dd0 @book=#<..., days=12|} in Object#total_fees at e04_debug_target.rb:16 #=> 300
  #1	[C] Array#sum at #<none>:0
  # and 2 frames (use `bt' command for all frames)
0: fee = 300
(rdbg) c    # continue command
total: 300

Things this session shows that are easy to miss:

  • break Object#late_fee ... printed "Unknown method name" and registered a pending breakpoint, because the method is defined later in the file; it resolved when the definition ran. A top-level method belongs to Object.

  • A line breakpoint on a line with no code moves to the next breakable line: in a separate run of the same file, break 11 (a blank line) reported the breakpoint on line 12.

  • The prompt of the source frame shows Object#late_fee(days_late=12): the arguments are printed with their values, and long values are abbreviated.

  • next moved over fee = 1000 if ...; finish ran until the method returned and printed the returned value after #=>.

  • [C] Array#sum is a frame of a method implemented in C, which has no Ruby source (up onto one reports "No sourcefile available").

The debug commands

At the (rdbg) prompt any line that is not a command is evaluated as Ruby and printed with pp; p expr, pp expr and eval expr do the same explicitly. Single-letter variables such as i, b, n or c are also commands, so use p c to see a variable called c. Pressing Enter repeats the last stepping command. Most commands have a short form, written s[tep] for s or step.

Command Effect

Control flow

s[tep], s[tep] n

Step into the next method call or line (n times)

n[ext], n[ext] n

Step over: run until the next line of the current frame

fin[ish], fin[ish] n

Run until the current frame (or the nth) returns

u[ntil], u[ntil] line, u[ntil] name

Run to a later line, to a given file:line, or until a method with that name is called

c, cont, continue

Resume

q[uit], q[uit]!, kill, kill!

Quit the debugger and the program (asking first, unless !); for a remote session quit leaves the program running and kill stops it

sigint

Run the program’s own SIGINT handler after the debugger caught Ctrl-C

Breakpoints

b[reak]

List the breakpoints

b[reak] line, b[reak] file:line

Stop at a line of the current or another file

b[reak] Class#method, b[reak] obj.method

Stop when a method is called

b[reak] ... if: expr

Stop only when the expression is truthy

b[reak] ... pre: command, ... do: command

Run a debugger command before stopping, or run it and continue without stopping

b[reak] ... path: /regexp/

Stop only if the file matches

catch Class

Stop when an exception of that class is raised (also with if:, pre:, do: and path:)

watch @ivar

Stop when an instance variable changes (slow)

del[ete], del[ete] n

Delete all breakpoints (asks first) or the numbered one

Looking around

bt, bt n, bt /regexp/

The backtrace, the first n frames, or the frames that match

l[ist], l[ist] -, l[ist] a-b

Show source around the current line, before it, or a range

i[nfo], i[nfo] locals, ivars, consts, globals, threads, breakpoints

Show the local variables (and %self), instance variables, constants, globals, threads or breakpoints; add /regexp/ to filter

o[utline], ls, ls expr

Methods, constants and variables of the context or of an object

display expr, undisplay [n]

Evaluate an expression at every stop; remove it

whereami, edit

Show the current frame with source; open the file in $EDITOR

f[rame], f[rame] n, up, down

Show or select a frame; evaluation then happens in that frame

th[read], th[read] n

List threads, or switch to one

Evaluating and tracing

p expr, pp expr, eval expr

Evaluate in the current frame

irb

Switch the console to IRB (irb:rdbg)

trace line, trace call, trace exception, trace object expr

Print every line, call and return, raise, or use of an object; add a /regexp/ filter or into: file; trace off n stops one

record on, record off, step back, step reset

Record execution and replay it backwards (locals are not available during replay)

config, config set name value, config unset name

Show or change settings (see below)

source file, open, open vscode, open chrome

Read commands from a file; open the debugger port for an editor or browser

h[elp], h[elp] command

Help

Stopping where the failure happens

An exception breakpoint is the debugger’s most useful tool when you do not know where a bad value came from. catch ArgumentError stops at the raise, before any rescue runs, with the whole call stack still available. The target below parses three ISBNs and rescues the bad ones, which hides exactly the information you want:

# frozen_string_literal: true

require "debug"
require "shelf"

raw = ["978-0-596-51774-8", "978-0-596-51774-9", "978-1-4493-9265-1"]

binding.break # rubocop:disable Lint/Debugger -- stops here and waits for debug commands

valid = raw.filter_map do |text|
  Shelf::Isbn.new(text)
rescue ArgumentError => e
  warn "skipped: #{e.message}"
  nil
end

puts valid.map(&:to_s)

Run it with ruby -I/path/to/shelf/lib e05_binding_break.rb; the program stops at binding.break, and the commands typed were catch ArgumentError, c, bt 4, p digits, frame 2, info locals, delete 0, c:

[3, 12] in e05_binding_break.rb
     3| require "debug"
     4| require "shelf"
     5|
     6| raw = ["978-0-596-51774-8", "978-0-596-51774-9", "978-1-4493-9265-1"]
     7|
=>   8| binding.break # rubocop:disable Lint/Debugger -- stops here and waits for debug commands
     9|
    10| valid = raw.filter_map do |text|
    11|   Shelf::Isbn.new(text)
    12| rescue ArgumentError => e
=>#0	<main> at e05_binding_break.rb:8
(rdbg) catch ArgumentError    # command
#0  BP - Catch  "ArgumentError"
(rdbg) c    # continue command
[3, 12] in /path/to/shelf/lib/shelf/isbn.rb
     3| module Shelf
     4|   # An ISBN-13 value object.
     5|   Isbn = Data.define(:digits) do
     6|     def initialize(digits:)
     7|       cleaned = digits.to_s.delete("-")
=>   8|       raise ArgumentError, "invalid ISBN: #{digits}" unless valid?(cleaned)
     9|
    10|       super(digits: cleaned)
    11|     end
    12|
=>#0	Shelf::Isbn#initialize(digits="978-0-596-51774-9") at /path/to/shelf/lib/shelf/isbn.rb:8
  #1	[C] Shelf::Isbn.new at #<none>:0
  # and 4 frames (use `bt' command for all frames)

Stop by #0  BP - Catch  "ArgumentError"
(rdbg) bt 4    # backtrace command
=>#0	Shelf::Isbn#initialize(digits="978-0-596-51774-9") at /path/to/shelf/lib/shelf/isbn.rb:8
  #1	[C] Shelf::Isbn.new at #<none>:0
  #2	block {|text="978-0-596-51774-9"|} in <main> at e05_binding_break.rb:11
  #3	[C] Array#each at #<none>:0
  # and 2 frames (use `bt' command for all frames)
(rdbg) p digits    # command
=> "978-0-596-51774-9"
(rdbg) frame 2    # command
=>  11|   Shelf::Isbn.new(text)
=>#2	block {|text="978-0-596-51774-9"|} in <main> at e05_binding_break.rb:11
(rdbg) info locals    # command
%self = main
text = "978-0-596-51774-9"
e = nil
raw = ["978-0-596-51774-8", "978-0-596-51774-9", "978-1-4493-9265-1"]
valid = nil
(rdbg) delete 0    # command
deleted: #0  BP - Catch  "ArgumentError"
(rdbg) c    # continue command
skipped: invalid ISBN: 978-0-596-51774-9
skipped: invalid ISBN: 978-1-4493-9265-1
9780596517748

catch stopped at the first failing ISBN (...-9) inside Shelf::Isbn#initialize, where p digits is the argument that failed validation; frame 2 moved to the caller’s block to see text; after delete 0 the program ran on and printed its own skipped: warnings. (The third ISBN is invalid as well, which the exception breakpoint would also have shown.) binding.break takes options: binding.break(pre: "p valid.size") runs a debug command at the stop and keeps stopped, and binding.break(do: "info ;; watch @count") runs the commands and continues, separated by ;; or \n, which turns a breakpoint into a probe that does not interrupt the run.

Scripting the debugger

Everything typed at the prompt can be given on the command line, which is how to use the debugger in a script, a Makefile or an automated agent. -e runs commands (separated by ;;) at the first stop, -x file runs a file of commands, RUBY_DEBUG_COMMANDS and RUBY_DEBUG_INIT_SCRIPT do the same from the environment, and ~/.rdbgrc (debug commands) and ~/.rdbgrc.rb (Ruby) run at every session. A final q! ends the session without a prompt:

$ rdbg --no-color -e 'break Object#late_fee ;; c ;; p days_late ;; bt ;; q!' -- -I/path/to/shelf/lib e04_debug_target.rb
(rdbg:commands) break Object#late_fee
Unknown method name: "late_fee"
#0  BP - Method (pending)  Object#late_fee
(rdbg:commands) c
...
Stop by #0  BP - Method  Object#late_fee at e04_debug_target.rb:6
(rdbg:commands) p days_late
=> 12
(rdbg:commands) bt
=>#0	Object#late_fee(days_late=12) at e04_debug_target.rb:7
  #1	block {|loan=#<Shelf::Loan:0x000000010ae771c8 @book=#<..., days=12|} in Object#total_fees at e04_debug_target.rb:14
  ...
(rdbg:commands) q!

Tracing is the other non-interactive tool. trace call /late_fee/ prints every call and return of a matching method, with the depth, thread and the returned value, without stopping:

$ rdbg --no-color -e 'trace call /late_fee/ ;; c' -- -I/path/to/shelf/lib e04_debug_target.rb
...
DEBUGGER (trace/call) #th:1 #depth:5 >     Object#late_fee at e04_debug_target.rb:6
DEBUGGER (trace/call) #th:1 #depth:5 <     Object#late_fee #=> 300 at e04_debug_target.rb:10
total: 300

Post-mortem and replay

config set postmortem true (or RUBY_DEBUG_POSTMORTEM=1) makes the debugger stop when the program ends with an uncaught exception, with the frames of the failure still inspectable. The program is a script that raises:

# frozen_string_literal: true

require "shelf"
Shelf::Isbn.new("not-an-isbn")

Run it with rdbg — -I/path/to/shelf/lib e09_postmortem.rb, then:

(rdbg) config set postmortem true    # command
# CONTROL: Enable postmortem debug (default: false)
postmortem = true
(rdbg) c    # continue command
Enter postmortem mode with #<ArgumentError: invalid ISBN: not-an-isbn>
  /path/to/shelf/lib/shelf/isbn.rb:8:in 'Shelf::Isbn#initialize'
  e09_postmortem.rb:4:in 'Shelf::Isbn.new'
  e09_postmortem.rb:4:in '<main>'

[3, 12] in /path/to/shelf/lib/shelf/isbn.rb
     3| module Shelf
     4|   # An ISBN-13 value object.
     5|   Isbn = Data.define(:digits) do
     6|     def initialize(digits:)
     7|       cleaned = digits.to_s.delete("-")
=>   8|       raise ArgumentError, "invalid ISBN: #{digits}" unless valid?(cleaned)
     9|
    10|       super(digits: cleaned)
    11|     end
    12|
=>#0	Shelf::Isbn#initialize(digits="not-an-isbn") at /path/to/shelf/lib/shelf/isbn.rb:8
  #1	[C] Shelf::Isbn.new at #<none>:0
  # and 1 frames (use `bt' command for all frames)
(rdbg:postmortem) bt 3    # backtrace command
=>#0	Shelf::Isbn#initialize(digits="not-an-isbn") at /path/to/shelf/lib/shelf/isbn.rb:8
  #1	[C] Shelf::Isbn.new at #<none>:0
  #2	<main> at e09_postmortem.rb:4
(rdbg:postmortem) p digits    # command
=> "not-an-isbn"

record on followed by stepping, then step back, replays the recorded lines backwards, which helps when you stepped one line too far. Replay shows positions only: during replay a local variable lookup fails with "undefined local variable" because the values are not recorded, so use it to find the line, not to read values.

Remote debugging

A program without a terminal (a daemon, a container, a process whose standard streams are pipes) can still be debugged. It opens a port, and a console attaches to it.

Way to open the port Notes

rdbg --open target.rb (-O)

Opens a UNIX domain socket and waits for a client at the first line; --nonstop does not wait

rdbg --open --port 12345 target.rb

TCP on 127.0.0.1:12345 instead (--host to change; --port-range n to try the next ports)

require "debug/open" or "debug/open_nonstop"

The same, from inside the program; RUBY_DEBUG_PORT selects TCP

RUBY_DEBUG_OPEN=true ruby -r debug target.rb

Opens the port with the environment configuration

rdbg -A, rdbg -A port, rdbg -A host port

Attach to a socket (the only one in the default directory), to a local port or to a remote host and port

On localhost with a TCP port, the two sides of a session look like this. The debuggee terminal prints the address and waits:

$ rdbg --open --port 12874 -- -I/path/to/shelf/lib e04_debug_target.rb
DEBUGGER: Debugger can attach via TCP/IP (127.0.0.1:12874)
DEBUGGER: wait for debugger connection...
DEBUGGER (client): Connected. PID:6522, $0:e04_debug_target.rb

and the attached console is an ordinary debug console, marked (rdbg:remote), that supports all the commands above:

(rdbg:remote) break Object#late_fee    # command
Unknown method name: "late_fee"
#0  BP - Method (pending)  Object#late_fee
(rdbg:remote) c    # continue command
[2, 11] in e04_debug_target.rb
     2|
     3| require "shelf"
     4|
     5| # Fee in cents: 25 per day late, capped at 1000.
     6| def late_fee(days_late)
=>   7|   fee = days_late * 25
     8|   fee = 1000 if fee > 1000
     9|   fee
    10| end
    11|
=>#0	Object#late_fee(days_late=12) at e04_debug_target.rb:7
  #1	block {|loan=#<Shelf::Loan:0x0000000124c10a00 @book=#<..., days=12|} in Object#total_fees at e04_debug_target.rb:14
  # and 3 frames (use `bt' command for all frames)

Stop by #0  BP - Method  Object#late_fee at e04_debug_target.rb:6
(rdbg:remote) info    # command
%self = main
days_late = 12
fee = nil
(rdbg:remote) bt 2    # backtrace command
=>#0	Object#late_fee(days_late=12) at e04_debug_target.rb:7
  #1	block {|loan=#<Shelf::Loan:0x0000000124c10a00 @book=#<..., days=12|} in Object#total_fees at e04_debug_target.rb:14
  # and 3 frames (use `bt' command for all frames)

quit in the attached console disconnects it and leaves the program running (and you can attach again), while kill! ends the program. Two warnings from the documentation apply. The messages between debugger and debuggee are not encrypted and the port is not authenticated beyond an optional --cookie, so keep the default of listening on the loopback or on a UNIX socket and tunnel with SSH when you must cross a network. And require "debug/open_nonstop" is a backdoor into the process when it is left in code that reaches production. RUBY_DEBUG_SKIP_BP=true makes binding.break calls skip when no client is attached, which keeps a service from blocking on a forgotten breakpoint.

Editor integration

The debugger speaks the Debug Adapter Protocol, so an editor can drive it. For Visual Studio Code, install the VSCode rdbg Ruby Debugger extension (publisher KoichiSasada) and have the debug gem and rdbg on the PATH:

  1. Open a .rb file, set breakpoints with F9 and press F5. With no configuration, the extension asks for a "Debug command line" (ruby foo.rb, rake test, bundle exec rspec, bin/rails s) and runs it under rdbg -c.

  2. For a repeatable setup, create .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "rdbg",
      "name": "Debug current file with rdbg",
      "request": "launch",
      "script": "${file}",
      "args": [],
      "askParameters": false
    },
    {
      "type": "rdbg",
      "name": "Run rake test",
      "request": "launch",
      "command": "rake",
      "script": "test",
      "args": [],
      "askParameters": false
    },
    {
      "type": "rdbg",
      "name": "Attach to localhost:12874",
      "request": "attach",
      "debugPort": "localhost:12874"
    }
  ]
}

The launch fields include script, command (such as bundle exec ruby), cwd, args, env, useBundler, askParameters, rdbgPath, debugPort and useTerminal (needed if the program reads standard input). The attach configuration takes debugPort, and localfsMap ("/app:${workspaceFolder}") when the program runs in a container that mounts the sources under another path. This file was written from the extension’s documentation and was not run, because no editor is involved in the checks of this page. Chrome DevTools can also attach: start rdbg --open=chrome target.rb, or type open chrome in a session, and open the printed address. Other editors reach the same protocol through their DAP clients.

Configuration

Every setting has an environment variable RUBY_DEBUG_<NAME> and a config name. The ones you will change most:

Setting (environment variable) Meaning

show_src_lines (RUBY_DEBUG_SHOW_SRC_LINES)

Source lines shown at a stop (default 10)

show_frames (RUBY_DEBUG_SHOW_FRAMES)

Frames shown at a stop (default 2)

no_color (RUBY_DEBUG_NO_COLOR, or NO_COLOR)

Disable colours, which you want when saving output

skip_path (RUBY_DEBUG_SKIP_PATH)

Frames and stops in these paths are skipped, such as a framework’s directory

skip_nosrc (RUBY_DEBUG_SKIP_NOSRC)

Skip stops in code that has no source

postmortem, keep_alloc_site

Post-mortem mode; remember where objects were allocated so p and pp show it

fork_mode (RUBY_DEBUG_FORK_MODE)

Which process keeps the debugger after fork: both, parent or child

nonstop, stop_at_load

Do not stop at the first line; stop as soon as the debugger loads

init_script, commands, no_rc

Commands to run at the first stop, and whether to read ~/.rdbgrc

open, port, host, sock_path, cookie, skip_bp

The remote options of the previous section

irb_console (RUBY_DEBUG_IRB_CONSOLE)

Use IRB as the console

history_file, save_history

Where command history is kept

log_level (RUBY_DEBUG_LOG_LEVEL)

Debugger’s own logging, for reporting bugs

Other variables: RUBY_DEBUG_ENABLE=0 stops require "debug" from enabling anything (useful in production images that still contain the call), RUBY_DEBUG_BB defines Kernel#bb as an alias of debugger, and RUBY_DEBUG_EDITOR or EDITOR select the editor for edit. config set changes a setting inside a session, and config lists them all with their current values.

Debugging under Bundler

A gem that is installed is not necessarily loadable inside bundle exec: Bundler restricts require to the gems in the Gemfile.lock. In Ruby 4.0 this now matters for both tools, because IRB left the default gems. With a Gemfile that does not list irb, binding.irb under bundle exec still works but prints a warning, and require "debug" fails:

$ bundle exec ruby t.rb
t.rb:3: warning: irb used to be loaded from the standard library, but is not part of the default gems since Ruby 4.0.0.
You can add irb to your Gemfile or gemspec to fix this error.
...
$ bundle exec ruby -e 'require "debug"'
.../ruby/4.0.0/bundled_gems.rb:60:in 'Kernel.require': cannot load such file -- debug (LoadError)

The fix is to list both in the development group:

# frozen_string_literal: true

source "https://rubygems.org"

gem "shelf", path: "../shelf"

group :development do
  gem "debug", platforms: %i[mri windows]
  gem "irb"
end

With that Gemfile, bundle exec ruby t.rb starts the IRB session without a warning, and rdbg -c — bundle exec rake test debugs a Bundler run. The platforms: option limits debug to CRuby (mri) and Windows. require: "debug/prelude" on the gem line is a lighter alternative to require "debug": the prelude only defines debugger and binding.break, and loads the debugger the first time one of them is called.

Debugging threads and forks

Debugger stops are per thread: info threads (or th) lists the threads, th 2 switches to one, and a breakpoint hit in one thread suspends the others while you inspect. fork_mode chooses whether the parent, the child or both keep a debugger after a fork. The README lists support for Ractors as not done yet, so do not rely on stepping inside one (see Ractors and Threads and the GVL).

pp and p

p is the oldest debugging tool and is often enough. Kernel#p writes inspect of each argument followed by a newline and returns its argument, so it can be put in the middle of an expression without changing it. Kernel#pp does the same but lays long values out over several lines. Both write to standard output (use warn or $stderr.puts if the output must not mix with the program’s).

# frozen_string_literal: true

require "shelf"

isbn = Shelf::Isbn.new("978-0-596-51774-8")
book = Shelf::Book.new(isbn:, title: "The Ruby Programming Language", author: Shelf::Author.new("Flanagan"))
catalog = Shelf::Catalog.new.add(book)

# p prints inspect and returns its argument, so it can be dropped into the middle of an expression.
count = p(catalog.count) + 1
puts count # => 2

# p with several arguments prints one line each and returns an array; with none it returns nil.
p(1, "two")           # prints 1 and "two"
p                     # prints nothing

# puts and print call to_s, which Data overrides here; inspect shows the structure.
puts book             # The Ruby Programming Language by Flanagan
p book                # one line, however long

# pp wraps at 80 columns and still returns its argument.
returned = pp(book)
puts returned.equal?(book) # => true

# pretty_inspect returns the pp text as a String (with a trailing newline).
puts book.pretty_inspect.lines.size

# The width can be chosen.
PP.pp(book, $stdout, 40)

# An instance-variable object: pp shows nested objects, indented.
pp catalog

Output:

1
2
1
"two"
The Ruby Programming Language by Flanagan
#<data Shelf::Book isbn=#<data Shelf::Isbn digits="9780596517748">, title="The Ruby Programming Language", author=#<data Shelf::Author name="Flanagan">>
#<data Shelf::Book
 isbn=#<data Shelf::Isbn digits="9780596517748">,
 title="The Ruby Programming Language",
 author=#<data Shelf::Author name="Flanagan">>
true
4
#<data Shelf::Book
 isbn=
  #<data Shelf::Isbn
   digits="9780596517748">,
 title="The Ruby Programming Language",
 author=
  #<data Shelf::Author name="Flanagan">>
#<Shelf::Catalog:0x0000000121e980f0
 @mutex=#<Thread::Mutex:0x0000000121e97e48>,
 @store=
  #<Shelf::Store::Memory:0x0000000121e97fd8
   @books=
    [#<data Shelf::Book
      isbn=#<data Shelf::Isbn digits="9780596517748">,
      title="The Ruby Programming Language",
      author=#<data Shelf::Author name="Flanagan">>]>>

The differences to remember:

  • p x prints x.inspect; puts x and print x print x.to_s (and puts also expands arrays one element per line). For an object whose to_s is overridden, p is the one that shows the structure.

  • p with one argument returns it, with several returns an array of them, with none returns nil and prints nothing. pp returns its argument in the same way. Because p returns its argument, p(x) + 1 and foo(p(bar)) are safe probes.

  • pp wraps at the terminal width minus one (it asks the output with io/console, then the COLUMNS variable, then assumes 80, so 79 when it cannot tell). With the output redirected to a file the width is 79 unless COLUMNS is set, which is why a pp in a test log can look different from the same call in a terminal. PP.pp(obj, out, width) chooses one, and PP.singleline_pp(obj, out) writes a single line.

  • pretty_inspect returns the pp text as a String, with the trailing newline. Kernel#pp is available without a require: it loads the pp library on first use.

  • Both call inspect on the objects. A default inspect shows every instance variable, which for a large object graph (an ActiveRecord-style model, a catalog with a mutex) is thousands of characters. Override inspect for a short form, or pretty_print for a layout, or limit the variables (pretty_print_instance_variables).

  • p(isbn:, author:) uses the value-omission shorthand of Ruby 3.1 to print {isbn: ..., author: ...}, a labelled print that names the variables. In Ruby 3.3 and older that hash prints as {:isbn=>...}; the new {isbn: ...} format is the Ruby 3.4 Hash#inspect.

Customising inspect and pretty_print

inspect is what IRB, p, error messages and the debugger show. pretty_print(q) is what pp, IRB’s result printing (IRB uses pp) and the debugger’s pp show. The q argument is a PP object with group, text, breakable, comma_breakable and seplist:

# frozen_string_literal: true

require "shelf"

# Reopening Catalog here only to show the two hooks; real code would put them in the class.
module Shelf
  class Catalog
    # `inspect` feeds p, IRB and error messages: keep it short and free of internals such as the mutex.
    def inspect = "#<Shelf::Catalog #{count} books>"

    # `pretty_print` feeds pp (and IRB). It receives a PP object and builds a layout.
    def pretty_print(pp)
      pp.group(1, "#<Shelf::Catalog", ">") do
        pp.breakable
        pp.seplist(to_a, -> { pp.comma_breakable }) { |book| pp.text(book.title.inspect) }
      end
    end
  end
end

isbn = Shelf::Isbn.new("978-0-596-51774-8")
author = Shelf::Author.new("Flanagan")
catalog = Shelf::Catalog.new
catalog.add(Shelf::Book.new(isbn:, title: "The Ruby Programming Language", author:))
catalog.add(Shelf::Book.new(isbn:, title: "Ruby Under a Microscope", author:))
catalog.add(Shelf::Book.new(isbn:, title: "Practical Object-Oriented Design", author:))

p catalog
pp catalog

# A labelled print: the shorthand hash makes the output name the variable.
p(isbn:, author:)

Output:

#<Shelf::Catalog 3 books>
#<Shelf::Catalog
 "The Ruby Programming Language",
 "Ruby Under a Microscope",
 "Practical Object-Oriented Design">
{isbn: #<data Shelf::Isbn digits="9780596517748">, author: #<data Shelf::Author name="Flanagan">}

Data and Struct already define both methods (#<data Shelf::Isbn digits="...">), which is why they read well at a prompt. Keep inspect free of secrets: it ends up in logs, exception messages (NoMethodError for older Rubies printed the receiver’s inspect) and IRB history.

Printing without a debugger

A few idioms cover most print debugging, and all of them keep working in a script that has no debugger, a container or a CI log:

# frozen_string_literal: true

require "shelf"

# A tiny tracing helper: prints to $stderr (so it never mixes with program output),
# names the caller, and returns the value so it can wrap any expression.
def dbg(value, label = nil)
  where = caller_locations(1, 1).first
  warn "[#{File.basename(where.path)}:#{where.lineno} #{where.base_label}] #{label || value.class}: #{value.inspect}"
  value
end

def title_letters(books)
  books.sum { |book| dbg(book.title.length, "title length") }
end

isbn = Shelf::Isbn.new("978-0-596-51774-8")
author = Shelf::Author.new("Flanagan")
books = [Shelf::Book.new(isbn:, title: "Ruby", author:), Shelf::Book.new(isbn:, title: "Rails", author:)]

# tap: look at an intermediate value without breaking the chain.
titles = books.map(&:title).tap { |list| warn "titles: #{list.inspect}" }.map(&:upcase)
puts titles.inspect

# dbg: wrap an expression, keep its value.
puts dbg(title_letters(books))

# warn with uplevel: reports the caller's line, like a library deprecation warning.
def old_title_letters(books)
  warn "old_title_letters is deprecated, use title_letters", uplevel: 1
  title_letters(books)
end
old_title_letters(books)

Standard output of this script:

["RUBY", "RAILS"]
9

and standard error:

titles: ["Ruby", "Rails"]
[e11_print_debugging.rb:14 title_letters] title length: 4
[e11_print_debugging.rb:14 title_letters] title length: 5
[e11_print_debugging.rb:26 <main>] Integer: 9
e11_print_debugging.rb:33: warning: old_title_letters is deprecated, use title_letters
[e11_print_debugging.rb:14 title_letters] title length: 4
[e11_print_debugging.rb:14 title_letters] title length: 5

tap returns its receiver after yielding it, so it can be inserted anywhere in a method chain; warn writes to $stderr so that the program’s real output stays clean; warn ..., uplevel: 1 prefixes the message with the caller’s file and line, the form libraries use for deprecation warnings; caller_locations(1, 1) identifies who called the current method. For a quick trace of a call chain, puts caller(0, 5) prints five lines of the stack, and ruby -d (or $DEBUG = true) prints each exception when it is raised, even those that are rescued, which finds an exception that is swallowed. See Reflection, Introspection and Tracing for TracePoint, which is the engine the debugger’s trace command is made of.

error_highlight

error_highlight (a default gem, loaded by default) adds to the message of NoMethodError and NameError, and to some TypeError and ArgumentError messages, a snippet of the source line and a row of carets under the exact part that failed. It is useful when one line has several calls, which is the usual way to get a NoMethodError on nil: the message says which of book.author.name failed. It needs the source file to be readable, so there is no snippet for code that comes from eval or -e.

Ruby 4.0 adds to ArgumentError (wrong number of arguments, unknown or missing keyword) two snippets: the caller, where the call is made, and the callee, where the method’s parameters are declared (Ruby 4.0 NEWS, Feature #21543). The script below triggers six common errors and prints each message the way an uncaught error would show it:

# frozen_string_literal: true

require "shelf"

isbn = Shelf::Isbn.new("978-0-596-51774-8")
book = Shelf::Book.new(isbn:, title: "The Ruby Programming Language", author: nil)
loan = Shelf::Loan.new(book)
stock = { "Ruby" => 3, "Rails" => 1 }

attempts = {
  "nil receiver" => -> { book.author.name },
  "method typo" => -> { loan.descibe },
  "variable typo" => -> { puts boook.title },
  "hash key typo" => -> { stock.fetch("Rubi") },
  "unknown constant" => -> { Shelf::Catlog.new },
  "bad keyword" => -> { Shelf::Loan.new(book, stat: :lent) }
}

attempts.each do |label, attempt|
  attempt.call
rescue StandardError => e
  puts "--- #{label}: #{e.class}"
  puts e.detailed_message(highlight: false)
end

Output on Ruby 4.0.7:

--- nil receiver: NoMethodError
undefined method 'name' for nil (NoMethodError)

  "nil receiver" => -> { book.author.name },
                                    ^^^^^
--- method typo: NoMethodError
undefined method 'descibe' for an instance of Shelf::Loan (NoMethodError)

  "method typo" => -> { loan.descibe },
                            ^^^^^^^^
Did you mean?  describe
--- variable typo: NameError
undefined local variable or method 'boook' for main (NameError)

  "variable typo" => -> { puts boook.title },
                               ^^^^^
Did you mean?  book
--- hash key typo: KeyError
key not found: "Rubi" (KeyError)
Did you mean?  "Ruby"
--- unknown constant: NameError
uninitialized constant Shelf::Catlog (NameError)

  "unknown constant" => -> { Shelf::Catlog.new },
                                  ^^^^^^^^
Did you mean?  Shelf::Catalog
--- bad keyword: ArgumentError
unknown keyword: :stat (ArgumentError)

    caller: e08_error_messages.rb:16
    |   "bad keyword" => -> { Shelf::Loan.new(book, stat: :lent) }
                                         ^^^^
    callee: /path/to/shelf/lib/shelf/loan.rb:10
    |     def initialize(book, state: :available)
              ^^^^^^^^^^

The same script gave different messages on older Rubies, which matters when you read an old log or a question on a forum:

Ruby book.author.name with author nil

3.2

undefined method `name' for nil:NilClass, with the snippet; a method on a plain object prints the whole inspect of the receiver

3.3

undefined method `name' for nil, and for an instance of Shelf::Loan instead of the receiver’s inspect

3.4

The same text with straight quotes, undefined method 'name' for nil

4.0

The same, and an ArgumentError for a method defined in Ruby gains the caller: and callee: snippets

ErrorHighlight.spot(exception) returns the location data (:snippet, :first_column, :last_column) so a tool can use it itself, and the gem formats through Exception#detailed_message, which is the right method to override to change what is shown for an error class. Pass highlight: false to get the text without terminal escape codes.

# frozen_string_literal: true

require "shelf"

isbn = Shelf::Isbn.new("978-0-596-51774-8")
loan = Shelf::Loan.new(Shelf::Book.new(isbn:, title: "Ruby", author: Shelf::Author.new("Flanagan")))

# did_you_mean: the suggestions are data on the exception.
begin
  loan.descibe
rescue NoMethodError => e
  puts e.corrections.inspect            # => [:describe]
  puts e.original_message               # message without the suggestions
end

# The spell checker is usable on its own, with any dictionary.
checker = DidYouMean::SpellChecker.new(dictionary: %w[available lent overdue returned])
puts checker.correct("overdu").inspect  # => ["overdue"]
puts checker.correct("zzz").inspect     # => []

# error_highlight: ask for the location of the failing expression in the source.
begin
  loan.book.author.name.upcase.nope
rescue NoMethodError => e
  spot = ErrorHighlight.spot(e)
  puts spot[:snippet].strip
  puts "columns #{spot[:first_column]}...#{spot[:last_column]}"
  puts e.detailed_message(highlight: false).lines.first
end

Output:

[:describe]
undefined method 'descibe' for an instance of Shelf::Loan
["overdue"]
[]
loan.book.author.name.upcase.nope
columns 30...35
undefined method 'nope' for an instance of String (NoMethodError)

message is the bare text and does not include the snippet or the suggestions: those are added by detailed_message, and full_message combines it with the backtrace. A test that compares e.message is therefore not affected by either gem, while a test that compares e.detailed_message or the text of an uncaught error is.

Disable it with ruby --disable=error_highlight (or RUBYOPT=--disable=error_highlight), for example while debugging an exception object itself.

did_you_mean

did_you_mean (also a default gem and loaded by default) appends Did you mean? and up to a few candidates to NameError, NoMethodError, KeyError, NoMatchingPatternKeyError and LoadError (for require paths). The suggestions come from a Jaro-Winkler and Levenshtein comparison against the methods, local variables, constants, hash keys or paths that exist at that point. The output above shows each case: descibe suggests describe, boook suggests the local variable book, "Rubi" suggests the key "Ruby", and Shelf::Catlog suggests Shelf::Catalog.

The data is available to code. e.corrections returns the candidates, e.original_message the message without them, and the spell checker works on any list of words (see the example in the previous section): DidYouMean::SpellChecker.new(dictionary: words).correct("input") returns the close matches, which is how a command-line program can suggest a mistyped subcommand. DidYouMean.correct_error(MyError, MyChecker) registers a checker for an error class of your own.

$ ruby --disable=did_you_mean oops.rb
oops.rb:2:in '<main>': undefined method 'descibe' for an instance of Shelf::Loan (NoMethodError)

Shelf::Loan.new(nil).descibe
                    ^^^^^^^^
$ ruby --disable=error_highlight oops.rb
oops.rb:2:in '<main>': undefined method 'descibe' for an instance of Shelf::Loan (NoMethodError)
Did you mean?  describe

A suggestion is a guess: it only compares names and does not know your intent, and a "Did you mean?" is sometimes misleading (a nearly identical method on a different class). To explore the names yourself, use ls obj in IRB, obj.methods.grep(/pattern/) and Module#instance_methods(false).

syntax_suggest

When a file does not parse, Ruby 3.4 and later report the error with Prism, which marks the unmatched keyword and the point where it noticed. syntax_suggest (a default gem since Ruby 3.2, loaded by default) adds a better guess for the most common failure in a long file, a missing end. It re-parses blocks by indentation and prints the block that looks unbalanced, with line numbers. The file below is missing the end of the block in titles:

# frozen_string_literal: true

module Shelf
  class Report
    def initialize(catalog)
      @catalog = catalog
    end

    def titles
      @catalog.map do |book|
        book.title.upcase
    end

    def count = @catalog.count
  end
end

(This file is not syntactically valid; it is the input of the demonstration.)

$ ruby bad_syntax.rb
bad_syntax.rb: --> bad_syntax.rb

Unmatched keyword, missing `end' ?

   3  module Shelf
   4    class Report
>  9      def titles
> 10        @catalog.map do |book|
> 12      end
  15    end
  16  end

bad_syntax.rb:16: syntax errors found (SyntaxError)
   1 | # frozen_string_literal: true
   2 |
>  3 | module Shelf
     | ^~~~~~ expected an `end` to close the `module` statement
   4 |   class Report
   5 |     def initialize(catalog)
  ~~~~~~
  14 |     def count = @catalog.count
  15 |   end
> 16 | end
     |    ^ unexpected end-of-input, assuming it is closing the parent top level context

The first part is syntax_suggest: it narrows the problem to def titles and @catalog.map do, whose do has no end. The second part is the parser’s own message. With ruby --disable=syntax_suggest only the second part is printed, and Ruby 3.2 prints the older syntax error, unexpected end-of-input form after the suggestion. The gem also provides a syntax_suggest command that takes a file name, and SyntaxSuggest.call for tools. Its guess is a heuristic based on indentation, so it works best on a consistently indented file; when the highlighted block looks right, look at the line above it.

Choosing a tool

Situation Tool Why

"What does this method return?", "what methods does this object have?"

IRB (ls, show_source, show_doc)

Immediate, no program to run

A value looks wrong in the middle of a method

p, pp, or binding.irb

Cheapest; binding.irb also lets you try the next line

An exception is rescued and hidden somewhere

catch ExceptionClass in rdbg, or ruby -d

Stops at the raise, with the stack intact

You do not know the call path to a method

trace call /name/ in rdbg, or caller in the method

Shows who calls it, and in what order

An instance variable changes unexpectedly

watch @ivar in rdbg

Stops at the assignment (it is slow, so narrow the scope)

A process without a terminal

rdbg --open and rdbg -A, or VS Code attach

Debugger and program talk over a socket

A test run

rdbg -c — bundle exec rake test, or a binding.break in the test

The failing test is the reproduction

A crash on startup that leaves no process

Post-mortem mode

The frames of the failure stay inspectable

Where the books are outdated

Older books and blog posts on debugging Ruby describe tools that have since changed or gone:

Old advice Ruby 4.0 reality

ruby -r debug opening the (rdb:1) debugger of the standard library, with its own command set

That library was replaced. require "debug" loads the debug gem, whose console is (rdbg) and whose commands are in this page. Under Bundler it fails with a LoadError unless debug is in the Gemfile

byebug, ruby-debug, debugger and pry plus pry-byebug as the standard debugging setup; binding.pry

byebug is superseded by debug; binding.irb and binding.break cover what binding.pry was used for. These gems still exist and are community-maintained; this page does not cover them

require "irb/completion" and require "irb/ext/save-history" in .irbrc, plus IRB.conf[:AUTO_INDENT] = true

Completion and history are built into the Reline editor and are on by default; irb/ext/save-history no longer exists (require raises LoadError) and history is controlled with IRB.conf[:SAVE_HISTORY]

IRB is a part of the standard library, irb always available

IRB is a bundled gem in 4.0, and with Bundler it must be in the Gemfile

Tab completion by regular expressions; readline as the line editor

Reline is the editor, and the type-based completor is the default on 3.4 and later. readline is itself a bundled gem

=> {:a=>1} in every REPL transcript

=> {a: 1} since 3.4, so copy-pasted outputs in old text do not match what you see

undefined method `x' for nil:NilClass in error messages and NoMethodError with the receiver’s whole inspect

undefined method 'x' for nil and for an instance of Foo (3.3 and 3.4)

require "pp" before using pp

pp is available without require, and wraps at the terminal width

ruby -w and $DEBUG as the way to see problems

Still valid, plus Warning[:category] switches and the tools above (see Reflection, Introspection and Tracing)