Debugging Ruby
Prerequisites: Testing with Minitest
Debugging is finding out why code doesn't do what you expect. Ruby gives you tools from quick-and-simple to fully interactive.
1. Read the error message and backtrace
app.rb:12:in 'Order#total': undefined method '*' for nil (NoMethodError)
quantity * price
^
from app.rb:20:in '<main>'
Read it top to bottom: where (app.rb line 12, in Order#total),
what (* was called on nil), and how we got there (the from
lines, called from line 20). Here, quantity was nil — the question
becomes "why wasn't it set?".
2. p and pp
Printing values at key points is often enough. Use p rather than
puts — it shows nil and quotes. pp pretty-prints large nested
hashes. p also returns its argument, so you can insert it mid-
expression: total = p(subtotal) * 1.2.
3. An interactive breakpoint: binding.irb
Put binding.irb on a line, and when execution reaches it, the program
pauses and opens an irb session inside that spot. You can inspect
every local variable, call methods, and try fixes:
def total(items)
subtotal = items.sum { |i| i[:price] }
binding.irb
subtotal * 1.2
end
Type exit to continue running.
4. The debug gem
Ruby ships with the debug gem, a full debugger with stepping:
require "debug"
def total(items)
binding.break # or: debugger
items.sum { |i| i[:price] }
end
At the prompt: next (run the next line), step (step into a method),
continue, info (show local variables), bt (backtrace). VS Code's
Ruby LSP extension can drive the same debugger with clickable
breakpoints.
A debugging process
- Reproduce the problem reliably — ideally as a failing test.
- Narrow it down: check values halfway through, and keep halving.
- Question assumptions: "this can't be
nil" is often exactly the problem. - Fix it, and keep the test, so the bug can't come back.
Common mistakes
- Leaving
binding.irbordebuggerin committed code. RuboCop'sLint/Debuggercheck catches this. - Changing several things at once while debugging, so you never learn which change fixed it.