Skip to content
AgileLogics
5 min read

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

  1. Reproduce the problem reliably — ideally as a failing test.
  2. Narrow it down: check values halfway through, and keep halving.
  3. Question assumptions: "this can't be nil" is often exactly the problem.
  4. Fix it, and keep the test, so the bug can't come back.

Common mistakes

  • Leaving binding.irb or debugger in committed code. RuboCop's Lint/Debugger check catches this.
  • Changing several things at once while debugging, so you never learn which change fixed it.