Back to Scaling Ruby Applications guides

Debug Ruby Applications with pry

Stanley Ulili
Updated on November 11, 2025

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:

Gemfile
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:

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
Output
[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
Output
[1] pry(main)>

When pry launches this way:

  1. Your script executes completely before the prompt appears
  2. All methods and constants from your script remain available in the session
  3. The pry prompt [1] pry(main)> indicates you're in the main context
  4. You can call any methods defined in your script interactively

From here, you can interact with your code using pry's command set:

  • show-source or $: Display the source code of a method
  • ls: List methods and variables available in the current scope
  • cd: Navigate into an object's context
  • whereami: Show your current location in the code
  • exit: Leave the pry session
  • help: 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?
Output
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)
Output
=> true
 
[3] pry(main)> prime?(18)
Output
=> false

The ls command reveals what's available in your current scope:

 
[4] pry(main)> ls
Output
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
Output
...
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? }
Output
=> [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:

prime_checker.rb
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:

Output
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:

  1. Pry displays the surrounding code with line numbers
  2. An arrow => marks the current line where execution is paused
  3. You have access to all local variables in the current scope
  4. You can inspect method parameters and modify values to test different scenarios

Check the current value of the number parameter:

 
[1] pry(main)> number
Output
=> 3

You can see all local variables in scope:

 
[2] pry(main)> ls -l
Output
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:

Output
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.

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:

calculator.rb
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:

Output
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
Output
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
Output
=> 5
 
[2] pry(main)> b
Output
=> 10

The next command executes the current line and moves to the next one within the same method:

 
[3] pry(main)> next
Output
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
Output
=> 50

The finish command completes the current method and returns to the caller:

 
[5] pry(main)> finish
Output
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
Output
=> 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:

stack_example.rb
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:

Output
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
Output
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
Output
=> "Deep in the stack"

The up command moves one level up the call stack:

 
[3] pry(main)> up
Output
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
Output
=> [1, 2, 3]

Move up another level:

 
[5] pry(main)> up
Output
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
Output
=> 10

The down command moves back down the stack:

 
[7] pry(main)> down
Output
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
Output
--> #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.

Got an article suggestion? Let us know
Next article
Top 10 Ruby on Rails Alternatives for Web Development
Discover the top 10 Ruby on Rails alternatives. Compare Django, Laravel, Express.js, Spring Boot & more with detailed pros, cons & features.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.