Debug Ruby Applications with pry
Debugging Ruby applications becomes significantly easier when you have tools that provide insight into your code's execution. While Ruby ships with a basic debugger, developers often reach for more powerful alternatives when troubleshooting complex issues.
pry is a runtime developer console for Ruby that replaces the default IRB (Interactive Ruby) shell. It offers syntax highlighting, robust introspection capabilities, and a plugin ecosystem that extends its functionality well beyond basic debugging.
With pry, you can pause execution at any point in your code, inspect objects and variables, navigate the call stack, and even modify running code to test fixes immediately.
This article will walk you through using pry to debug Ruby applications effectively.
Prerequisites
Before you begin, make sure you have:
- Ruby 3.3+ installed on your machine
- Familiarity with basic Ruby syntax and application structure
Setting up the project directory
You'll create a clean workspace for practicing debugging techniques with pry.
Start by creating a directory for the debugging examples and navigate into it:
mkdir ruby_debugging && cd ruby_debugging
Ruby's bundler manages gems on a per-project basis, so initialize a new bundle:
bundle init
This creates a Gemfile where you can specify project dependencies. Open the Gemfile and add pry:
source 'https://rubygems.org'
gem 'pry'
Install the gem:
bundle install
The gem is now available for your debugging session, isolated from other Ruby projects on your system.
Debugging Ruby scripts with pry from the command line
The pry gem transforms Ruby's debugging experience by providing an enhanced REPL (Read-Eval-Print Loop) with features like command history, syntax highlighting, and powerful introspection tools. You can launch pry directly to debug scripts without modifying your code.
Create a Ruby script that calculates prime numbers. Save this as prime_checker.rb:
def prime?(number)
return false if number < 2
return true if number == 2
(2..Math.sqrt(number).to_i).each do |divisor|
return false if number % divisor == 0
end
true
end
def primes_up_to(limit)
(2..limit).select { |n| prime?(n) }
end
puts primes_up_to(50).inspect
Running this normally produces a list of prime numbers up to 50:
ruby prime_checker.rb
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
To debug this script with pry, you can load it directly into a pry session:
pry -r ./prime_checker.rb
[1] pry(main)>
When pry launches this way:
- Your script executes completely before the prompt appears
- All methods and constants from your script remain available in the session
- The pry prompt
[1] pry(main)>indicates you're in the main context - You can call any methods defined in your script interactively
From here, you can interact with your code using pry's command set:
show-sourceor$: Display the source code of a methodls: List methods and variables available in the current scopecd: Navigate into an object's contextwhereami: Show your current location in the codeexit: Leave the pry sessionhelp: Display available commands
These commands let you explore your code's structure interactively. Try examining the prime? method and then press ENTER:
[1] pry(main)> show-source prime?
From: /Users/stanley/ruby_debugging/prime_checker.rb:1:
Owner: Object
Visibility: private
Signature: prime?(number)
Number of lines: 10
def prime?(number)
return false if number < 2
return true if number == 2
(2..Math.sqrt(number).to_i).each do |divisor|
return false if number % divisor == 0
end
true
end
You can test methods directly with different inputs:
[2] pry(main)> prime?(17)
=> true
[3] pry(main)> prime?(18)
=> false
The ls command reveals what's available in your current scope:
[4] pry(main)> ls
self.methods: inspect to_s
locals: _ __ _dir_ _ex_ _file_ _in_ _out_ pry_instance
To see detailed information about a method, including where it's defined:
[5] pry(main)> show-source primes_up_to
...
Number of lines: 3
def primes_up_to(limit)
(2..limit).select { |n| prime?(n) }
end
One of pry's strengths is its ability to navigate into object contexts. You can examine what methods a number responds to:
[6] pry(main)> cd 42
[7] pry(42):1> ls
This displays all methods available on the integer object. Press q to exit the list view, then return to the main context:
[8] pry(42):1> cd ..
[9] pry(main)>
You can also evaluate Ruby expressions directly to test logic:
[10] pry(main)> (2..10).select { |n| n.even? }
=> [2, 4, 6, 8, 10]
This interactive exploration helps you understand how your code behaves without repeatedly editing and running the script.
Embedding pry breakpoints in your code
Rather than loading your entire script into pry, you can pause execution at specific points by inserting breakpoints directly in your code. This approach works better when:
- You've identified a specific area where bugs likely exist
- You want to inspect variable states at precise moments during execution
- Your application has complex initialization that makes command-line debugging impractical
- You're debugging code that's triggered by external events or user input
Here's how to add a breakpoint to your Ruby code:
require 'pry'
def prime?(number)
return false if number < 2
return true if number == 2
binding.pry # Execution pauses here
(2..Math.sqrt(number).to_i).each do |divisor|
return false if number % divisor == 0
end
true
end
def primes_up_to(limit)
(2..limit).select { |n| prime?(n) }
end
puts primes_up_to(50).inspect
When you run this script:
ruby prime_checker.rb
Execution stops at the binding.pry line, dropping you into an interactive session:
From: /Users/stanley/prime_checker.rb:7 Object#prime?:
3: def prime?(number)
4: return false if number < 2
5: return true if number == 2
6:
=> 7: binding.pry # Execution pauses here
8:
9: (2..Math.sqrt(number).to_i).each do |divisor|
10: return false if number % divisor == 0
11: end
[1] pry(main)>
Several things happen at this breakpoint:
- Pry displays the surrounding code with line numbers
- An arrow
=>marks the current line where execution is paused - You have access to all local variables in the current scope
- You can inspect method parameters and modify values to test different scenarios
Check the current value of the number parameter:
[1] pry(main)> number
=> 3
You can see all local variables in scope:
[2] pry(main)> ls -l
number = 3
To continue execution until the next breakpoint or the program ends, use the exit command:
[3] pry(main)> exit
The program will hit the breakpoint again for the next number being checked. You can see the updated value:
From: /Users/stanley/prime_checker.rb:7 Object#prime?:
3: def prime?(number)
4: return false if number < 2
5: return true if number == 2
6:
=> 7: binding.pry
8:
9: (2..Math.sqrt(number).to_i).each do |divisor|
10: return false if number % divisor == 0
11: end
[1] pry(main)> number
=> 4
Type exit-program to stop execution entirely and exit the script.
Using Ruby's built-in debug gem
Ruby 3.1 introduced a built-in debug gem that works similarly to pry. If you prefer not to add external dependencies, you can use the debugger keyword:
# No require statement needed
def prime?(number)
return false if number < 2
return true if number == 2
debugger # Uses Ruby's built-in debugger
(2..Math.sqrt(number).to_i).each do |divisor|
return false if number % divisor == 0
end
true
end
This approach has trade-offs:
- Zero setup: No gems to install or require statements needed
- Different interface: The commands differ from pry's syntax
- Less powerful: Fewer introspection and navigation features
- Standard across environments: Available wherever Ruby 3.1+ runs
For this guide, we'll continue using pry since it offers more debugging capabilities and a better interactive experience.
Navigating execution with step commands
When debugging complex logic, you often need to trace execution step by step. Standard pry doesn't include step commands, but the pry-byebug gem adds this functionality.
Install pry-byebug:
gem install pry-byebug
Create a new file that demonstrates nested method calls:
require 'pry-byebug'
def multiply(a, b)
result = a * b
result
end
def calculate_area(length, width)
binding.pry
area = multiply(length, width)
area
end
puts "Area: #{calculate_area(5, 10)}"
Run the script:
ruby calculator.rb
When execution pauses at the breakpoint, pry-byebug automatically positions you at the next executable line:
From: /Users/stanley/calculator.rb:10 Object#calculate_area:
8: def calculate_area(length, width)
9: binding.pry
=> 10: area = multiply(length, width)
11: area
12: end
[1] pry(main)>
The step command lets you step into method calls:
[1] pry(main)> step
From: /Users/stanley/calculator.rb:4 Object#multiply:
3: def multiply(a, b)
=> 4: result = a * b
5: result
6: end
[1] pry(main)>
You're now inside the multiply method. Check the parameter values:
[1] pry(main)> a
=> 5
[2] pry(main)> b
=> 10
The next command executes the current line and moves to the next one within the same method:
[3] pry(main)> next
From: /Users/stanley/calculator.rb:5 Object#multiply:
3: def multiply(a, b)
4: result = a * b
=> 5: result
6: end
[3] pry(main)>
Check the result that was just calculated:
[4] pry(main)> result
=> 50
The finish command completes the current method and returns to the caller:
[5] pry(main)> finish
From: /Users/stanley/calculator.rb:11 Object#calculate_area:
8: def calculate_area(length, width)
9: binding.pry
10: area = multiply(length, width)
=> 11: area
12: end
[5] pry(main)>
You're back in calculate_area, and the area variable now holds the return value from multiply:
[6] pry(main)> area
=> 50
These navigation commands give you precise control over execution flow, letting you trace exactly how data moves through your program.
Inspecting state with stack navigation
When debugging method chains or nested calls, understanding the call stack becomes critical. Pry's stack navigation commands let you move up and down the call stack to inspect variables at different execution levels.
Create a script that demonstrates a deeper call chain:
require 'pry-byebug'
def level_three
message = "Deep in the stack"
binding.pry
message
end
def level_two
data = [1, 2, 3]
level_three
end
def level_one
count = 10
level_two
end
level_one
Run the script:
ruby stack_example.rb
Execution pauses inside level_three:
From: /Users/stanley/stack_example.rb:6 Object#level_three:
4: def level_three
5: message = "Deep in the stack"
=> 6: binding.pry
7: message
8: end
[1] pry(main)>
The whereami command shows your current position with more context:
[1] pry(main)> whereami
From: /Users/stanley/stack_example.rb:6 Object#level_three:
1: require 'pry-byebug'
2:
3: def level_three
4: message = "Deep in the stack"
5: binding.pry
=> 6: message
7: end
You can access the local message variable:
[2] pry(main)> message
=> "Deep in the stack"
The up command moves one level up the call stack:
[3] pry(main)> up
Frame number: 1/4
From: /Users/stanley/stack_example.rb:11 Object#level_two:
10: def level_two
11: data = [1, 2, 3]
=> 12: level_three
13: end
You're now in level_two's context. You can access its local variables:
[4] pry(main)> data
=> [1, 2, 3]
Move up another level:
[5] pry(main)> up
Frame number: 2/4
From: /Users/stanley/stack_example.rb:17 Object#level_one:
15: def level_one
16: count = 10
=> 17: level_two
18: end
Now you can see variables from level_one:
[6] pry(main)> count
=> 10
The down command moves back down the stack:
[7] pry(main)> down
Frame number: 1/4
From: /Users/stanley/stack_example.rb:12 Object#level_two:
10: def level_two
11: data = [1, 2, 3]
=> 12: level_three
13: end
You're back in level_two. The backtrace command shows the complete call stack:
[8] pry(main)> backtrace
--> #0 Object#level_two at /Users/stanley/stack_example.rb:12
#1 Object#level_one at /Users/stanley/stack_example.rb:17
#2 <main> at /Users/stanley/stack_example.rb:20
This navigation ability proves valuable when debugging complex applications where understanding the sequence of method calls helps identify where things go wrong.
Final thoughts
As you’ve seen throughout this guide, Pry brings a new level of control and visibility to debugging Ruby applications. Instead of relying on simple print statements or Ruby’s basic debugger, Pry allows you to pause execution, inspect objects, and explore your code interactively.
By integrating tools like pry and pry-byebug, you can step through execution, navigate the call stack, and test fixes in real time. This hands-on approach not only speeds up troubleshooting but also deepens your understanding of how your Ruby code actually runs.
In the end, adopting Pry transforms debugging from a tedious task into a powerful, exploratory process that helps you write cleaner, more reliable Ruby applications.