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
debuggem (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,inspectand 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).
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 |
|---|---|---|
|
An interactive Ruby shell with multi-line editing, colour, completion, documentation lookup and |
Bundled gem since 4.0; a default gem up to 3.4. Under Bundler it must be in the |
|
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 |
|
Printing a value for a developer |
Core ( |
|
Adds the failing expression and a caret line to error messages |
Default gem, loaded by default |
|
Adds spelling suggestions to error messages |
Default gem, loaded by default |
|
Explains |
Default gem since 3.2, loaded by default |
|
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 |
|---|---|
|
Do not load any configuration file |
|
|
|
Print (default) or do not print the value of each expression |
|
Print, hide or shorten the value of an assignment. The default is truncated, which is why a large |
|
Print the result with |
|
Choose the line editor: the multi-line Reline editor (default), the single-line one, or none |
|
Syntax and result colours (default on; the |
|
The completion dialog that appears while you type (default on) |
|
Choose how completion candidates are computed (see Type-based completion) |
|
Print long output (such as |
|
Choose the prompt ( |
|
How the binding of a new workspace is created (0 to 5) |
|
Treat the first argument as a script (default) or leave all arguments in |
|
Number of backtrace lines kept at each end of an error, and a stack trace for every command |
|
Share |
|
Add an RI documentation directory to the documentation dialog |
|
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.
Source: IRB documentation
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 |
|---|---|
|
List the commands, or describe one |
|
Show the methods, constants, instance variables and local variables of an object or the current context, grouped by the class that defines them; |
|
Print the source code of a method, class, module or constant ( |
|
Look the name up in the RI documentation and show it; with no argument start an interactive |
|
Show the source around the line where |
|
Make |
|
Change, push, pop, list and show the current workspace (the same idea as |
|
The input history, optionally filtered; |
|
Print the wall time of each expression ( |
|
Open a file or the definition of a method in |
|
Copy the printed result to the clipboard (program overridable with |
|
The current configuration, and the Ruby, IRB, input method and completion in use |
|
Load or require a file inside the session. |
|
Leave the session (a |
|
Hand control to the |
|
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
Source: IRB documentation: Commands
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
Source: IRB documentation: Commands
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
Source: IRB documentation
Pitfalls in the REPL
-
The history is global. Every session appends to the same history file, by default
~/.irb_history, which is why thehistorynumbers in a long-lived shell are large.IRB.conf[:SAVE_HISTORY](default 1000 lines) andIRB.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 wordhistoryis the array. Usebinding.local_variablesto see what you have defined. -
Output is
inspect, notto_s. ADataorStructwhoseto_syou overrode still prints with its structure in IRB. -
Ctrl-C interrupts the running expression, not the shell (the default
IRB.conf[:IGNORE_SIGINT]istrue), 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
Source: IRB documentation: Binding#irb
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
Source: IRB documentation: Binding#irb
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
rescueclause (rescue => ethenbinding.irb), inside a block, or in a method called from a test.binding.irbin a test run waits forever in CI, so remove it before committing; RuboCop’sLint/Debuggercop exists to catch it. -
Under Bundler it needs
irbin theGemfile(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:
-
the path in the
IRBRCenvironment variable; -
$XDG_CONFIG_HOME/irb/irbrc; -
~/.irbrc; -
~/.config/irb/irbrc(only whenXDG_CONFIG_HOMEis not set); -
.irbrc,_irbrcor$irbrcin 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)
Source: IRB documentation: Configure IRB
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
>>
Source: IRB documentation: Extend IRB
The most useful IRB.conf entries and the environment variables behind them:
IRB.conf key |
Environment variable / option | Meaning |
|---|---|---|
|
Number of lines kept (default 1000; |
|
|
|
Prompt style; |
|
|
The automatic completion dialog |
|
|
Which completor computes candidates |
|
|
Colour output |
|
|
Send long output through a pager |
|
|
Print results; |
|
|
|
|
|
Libraries required at startup |
|
|
Additional RI directories for |
|
|
Backtrace lines kept and per-command tracing |
|
Hooks called when IRB exits |
|
|
|
More detail about IRB itself |
|
A hash of aliases such as |
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::Basesubclass registered withIRB::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. Usecategory,descriptionandhelp_messageto put it inhelp. -
A helper method (
IRB::HelperMethod::Basesubclass registered withIRB::HelperMethod.register) is an ordinary method call that returns an object (sample_catalog.first). It shows up under "Helper methods" inhelp, next to the built-inconf.
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.
Source: IRB documentation: Extend IRB
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. Aftertitles.map { it.it knowsitis a String. It is the default in IRB on Ruby 3.4 and later when the gem is available;IRB_COMPLETOR=regexp,--regexp-completororIRB.conf[:COMPLETOR] = :regexpswitches back, and--type-completorforces 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?"]
Source: repl_type_completor repository
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 callbinding.break(aliasesbinding.banddebugger) where you want to stop. The program runs normally until that line, then opens the debug console. It isbinding.irbwith a debugger instead of a REPL, andrequire "debug"is the same asrdbg --nonstop --no-sigint-hook. -
From the command line.
rdbg script.rb argsstartsscript.rblikeruby script.rb argsand stops at the first line;rdbg -n(--nonstop) does not stop there, so only your breakpoints fire.rdbg — -r foo -e codepasses interpreter options after--, as inrdbg — -I/path/to/shelf/lib target.rb:rdbghas no-Iof its own, andrdbg -I lib target.rbfails with "invalid option: -I". -
For another Ruby executable.
rdbg -c — bundle exec rake testruns the command withrdbg -cand the debugger attaches to the Ruby it starts. The--separates rdbg’s options from the command’s (rdbg -c rake -Twould be read asrdbg -c -T — rake). Under Bundler theGemfilemust listdebug.
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)}"
Source: ruby/debug repository
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 toObject. -
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. -
nextmoved overfee = 1000 if ...;finishran until the method returned and printed the returned value after#=>. -
[C] Array#sumis a frame of a method implemented in C, which has no Ruby source (uponto 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 |
|
|
Step into the next method call or line (n times) |
|
Step over: run until the next line of the current frame |
|
Run until the current frame (or the nth) returns |
|
Run to a later line, to a given |
|
Resume |
|
Quit the debugger and the program (asking first, unless |
|
Run the program’s own |
Breakpoints |
|
|
List the breakpoints |
|
Stop at a line of the current or another file |
|
Stop when a method is called |
|
Stop only when the expression is truthy |
|
Run a debugger command before stopping, or run it and continue without stopping |
|
Stop only if the file matches |
|
Stop when an exception of that class is raised (also with |
|
Stop when an instance variable changes (slow) |
|
Delete all breakpoints (asks first) or the numbered one |
Looking around |
|
|
The backtrace, the first n frames, or the frames that match |
|
Show source around the current line, before it, or a range |
|
Show the local variables (and |
|
Methods, constants and variables of the context or of an object |
|
Evaluate an expression at every stop; remove it |
|
Show the current frame with source; open the file in |
|
Show or select a frame; evaluation then happens in that frame |
|
List threads, or switch to one |
Evaluating and tracing |
|
|
Evaluate in the current frame |
|
Switch the console to IRB ( |
|
Print every line, call and return, raise, or use of an object; add a |
|
Record execution and replay it backwards (locals are not available during replay) |
|
Show or change settings (see below) |
|
Read commands from a file; open the debugger port for an editor or browser |
|
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 |
|---|---|
|
Opens a UNIX domain socket and waits for a client at the first line; |
|
TCP on |
|
The same, from inside the program; |
|
Opens the port with the environment configuration |
|
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:
-
Open a
.rbfile, 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 underrdbg -c. -
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 |
|---|---|
|
Source lines shown at a stop (default 10) |
|
Frames shown at a stop (default 2) |
|
Disable colours, which you want when saving output |
|
Frames and stops in these paths are skipped, such as a framework’s directory |
|
Skip stops in code that has no source |
|
Post-mortem mode; remember where objects were allocated so |
|
Which process keeps the debugger after |
|
Do not stop at the first line; stop as soon as the debugger loads |
|
Commands to run at the first stop, and whether to read |
|
The remote options of the previous section |
|
Use IRB as the console |
|
Where command history is kept |
|
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">>]>>
Source: Ruby documentation: Kernel#p
Source: Ruby documentation: Kernel#pp
The differences to remember:
-
p xprintsx.inspect;puts xandprint xprintx.to_s(andputsalso expands arrays one element per line). For an object whoseto_sis overridden,pis the one that shows the structure. -
pwith one argument returns it, with several returns an array of them, with none returnsniland prints nothing.ppreturns its argument in the same way. Becausepreturns its argument,p(x) + 1andfoo(p(bar))are safe probes. -
ppwraps at the terminal width minus one (it asks the output withio/console, then theCOLUMNSvariable, then assumes 80, so 79 when it cannot tell). With the output redirected to a file the width is 79 unlessCOLUMNSis set, which is why appin a test log can look different from the same call in a terminal.PP.pp(obj, out, width)chooses one, andPP.singleline_pp(obj, out)writes a single line. -
pretty_inspectreturns thepptext as a String, with the trailing newline.Kernel#ppis available without arequire: it loads thepplibrary on first use. -
Both call
inspecton the objects. A defaultinspectshows every instance variable, which for a large object graph (an ActiveRecord-style model, a catalog with a mutex) is thousands of characters. Overrideinspectfor a short form, orpretty_printfor 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.4Hash#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">}
Source: Ruby documentation: PP
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 |
|
3.3 |
|
3.4 |
The same text with straight quotes, |
4.0 |
The same, and an |
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
Source: Ruby documentation: DidYouMean
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 ( |
Immediate, no program to run |
A value looks wrong in the middle of a method |
|
Cheapest; |
An exception is rescued and hidden somewhere |
|
Stops at the |
You do not know the call path to a method |
|
Shows who calls it, and in what order |
An instance variable changes unexpectedly |
|
Stops at the assignment (it is slow, so narrow the scope) |
A process without a terminal |
|
Debugger and program talk over a socket |
A test run |
|
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 |
|---|---|
|
That library was replaced. |
|
|
|
Completion and history are built into the Reline editor and are on by default; |
IRB is a part of the standard library, |
IRB is a bundled gem in 4.0, and with Bundler it must be in the |
Tab completion by regular expressions; |
Reline is the editor, and the type-based completor is the default on 3.4 and later. |
|
|
|
|
|
|
|
Still valid, plus |
References
-
IRB documentation (the
irbgem), including command-line options, configuration and extending IRB -
ruby/debug repository: README, debug commands, configuration and remote debugging
-
ruby/vscode-rdbg repository: VS Code extension for the debug gem and Visual Studio Code: Debugging
-
Ruby documentation: ErrorHighlight, DidYouMean and SyntaxSuggest