Back to Scaling Ruby Applications guides

Debug Ruby Applications with Byebug

Stanley Ulili
Updated on November 11, 2025

When building Ruby applications, you'll inevitably encounter bugs that require more than print statements to diagnose. Effective debugging means understanding exactly what your code does at each step, examining variable states, and tracing execution flow through complex logic.

Byebug is a debugger for Ruby 2.x and 3.x that provides essential debugging capabilities including breakpoints, step-by-step execution, and stack navigation. It's built as a standalone gem that works independently of any REPL or interactive shell.

With Byebug, you can halt program execution at specific points, inspect the current state of variables, navigate through the call stack, and control how your code executes line by line.

This article will show you how to use Byebug to debug Ruby applications effectively.

Prerequisites

Before you start, ensure you have:

  • Ruby 2.0+ installed on your machine
  • Basic familiarity with Ruby syntax and application structure

Setting up the project directory

You'll create a workspace for practicing debugging techniques with Byebug.

Create a directory for the debugging examples and navigate into it:

 
mkdir ruby_byebug_debugging && cd ruby_byebug_debugging

Initialize a new bundle to manage dependencies:

 
bundle init

Open the generated Gemfile and add Byebug:

Gemfile
source 'https://rubygems.org'

gem 'byebug'

Install the gem:

 
bundle install

Byebug is now available for your debugging sessions, isolated from other Ruby projects on your system.

Understanding Byebug basics

Byebug operates differently from interactive shells like pry. While pry provides a full REPL environment, Byebug focuses specifically on debugging capabilities. This makes it lightweight and straightforward for traditional debugging workflows.

Create a simple Ruby script to explore Byebug's features. Save this as temperature_converter.rb:

temperature_converter.rb
def celsius_to_fahrenheit(celsius)
  fahrenheit = (celsius * 9.0 / 5.0) + 32
  fahrenheit
end

def fahrenheit_to_celsius(fahrenheit)
  celsius = (fahrenheit - 32) * 5.0 / 9.0
  celsius
end

def convert_temperatures(temps_celsius)
  temps_celsius.map { |temp| celsius_to_fahrenheit(temp) }
end

temperatures = [0, 10, 20, 30, 40]
converted = convert_temperatures(temperatures)

puts "Celsius: #{temperatures.inspect}"
puts "Fahrenheit: #{converted.inspect}"

Running this script normally produces temperature conversions:

 
ruby temperature_converter.rb
Output
Celsius: [0, 10, 20, 30, 40]
Fahrenheit: [32.0, 50.0, 68.0, 86.0, 104.0]

To debug with Byebug, you need to insert breakpoints directly in your code. There's no command-line option to launch Byebug like some other debuggers.

Adding breakpoints to your code

Byebug requires you to explicitly place breakpoints in your source code where you want execution to pause. This approach gives you precise control over which parts of your application you want to inspect.

Modify your temperature converter to include a breakpoint:

temperature_converter.rb
require 'byebug'
def celsius_to_fahrenheit(celsius)
byebug
fahrenheit = (celsius * 9.0 / 5.0) + 32 fahrenheit end def fahrenheit_to_celsius(fahrenheit) celsius = (fahrenheit - 32) * 5.0 / 9.0 celsius end def convert_temperatures(temps_celsius) temps_celsius.map { |temp| celsius_to_fahrenheit(temp) } end temperatures = [0, 10, 20, 30, 40] converted = convert_temperatures(temperatures) puts "Celsius: #{temperatures.inspect}" puts "Fahrenheit: #{converted.inspect}"

Run the modified script:

 
ruby temperature_converter.rb

Execution stops at the byebug statement:

Output
[1, 10] in temperature_converter.rb
    1: require 'byebug'
    2:
    3: def celsius_to_fahrenheit(celsius)
    4:   byebug
=>  5:   fahrenheit = (celsius * 9.0 / 5.0) + 32
    6:   fahrenheit
    7: end
    8:
    9: def fahrenheit_to_celsius(fahrenheit)
   10:   celsius = (fahrenheit - 32) * 5.0 / 9.0
(byebug)

When Byebug activates:

  1. It displays the code around your current location with line numbers
  2. An arrow => marks the next line to execute
  3. The (byebug) prompt appears, waiting for your commands
  4. You have access to all variables in the current scope

Check the value of the celsius parameter:

 
(byebug) celsius
Output
0

Byebug provides several essential commands for controlling execution:

  • continue or c: Resume execution until the next breakpoint or program end
  • next or n: Execute the current line and move to the next line
  • step or s: Step into method calls
  • finish or fin: Execute until the current method returns
  • quit or q: Exit the debugger and terminate the program
  • help: Display available commands

Execute the current line and move forward:

 
(byebug) next
Output
[1, 10] in temperature_converter.rb
    1: require 'byebug'
    2:
    3: def celsius_to_fahrenheit(celsius)
    4:   byebug
    5:   fahrenheit = (celsius * 9.0 / 5.0) + 32
=>  6:   fahrenheit
    7: end
    8:
    9: def fahrenheit_to_celsius(fahrenheit)
   10:   celsius = (fahrenheit - 32) * 5.0 / 9.0

Check the calculated result:

 
(byebug) fahrenheit
Output
32.0

Continue execution to hit the breakpoint again for the next temperature:

 
(byebug) continue
Output
[1, 10] in temperature_converter.rb
    1: require 'byebug'
    2:
    3: def celsius_to_fahrenheit(celsius)
    4:   byebug
=>  5:   fahrenheit = (celsius * 9.0 / 5.0) + 32
    6:   fahrenheit
    7: end

The breakpoint triggers again. Check the new value:

 
(byebug) celsius
Output
10

Type quit to exit the debugger and stop the program entirely.

Inspecting variables and expressions

Byebug lets you examine not just simple variables but also complex expressions and object properties. This capability helps you understand the exact state of your application at any point during execution.

Create a new script that works with more complex data:

student_grades.rb
require 'byebug'

def calculate_average(grades)
  byebug
  total = grades.sum
  average = total.to_f / grades.length
  average
end

def get_letter_grade(average)
  case average
  when 90..100 then 'A'
  when 80...90 then 'B'
  when 70...80 then 'C'
  when 60...70 then 'D'
  else 'F'
  end
end

students = [
  { name: 'Alice', grades: [95, 87, 92, 88] },
  { name: 'Bob', grades: [78, 82, 85, 80] },
  { name: 'Charlie', grades: [62, 58, 65, 70] }
]

students.each do |student|
  avg = calculate_average(student[:grades])
  letter = get_letter_grade(avg)
  puts "#{student[:name]}: #{avg.round(2)} (#{letter})"
end

Run the script:

 
ruby student_grades.rb

When execution pauses, you can inspect the grades array:

Output
[1, 10] in student_grades.rb
    1: require 'byebug'
    2:
    3: def calculate_average(grades)
    4:   byebug
=>  5:   total = grades.sum
    6:   average = total.to_f / grades.length
    7:   average
    8: end
(byebug) grades
[95, 87, 92, 88]

You can evaluate Ruby expressions directly:

 
(byebug) grades.length
Output
4
 
(byebug) grades.max
Output
95
 
(byebug) grades.min
Output
87

The display command automatically shows an expression's value after each step:

 
(byebug) display grades.sum
Output
1: grades.sum = 362

Now execute the next line:

 
(byebug) next
Output
1: grades.sum = 362

[1, 10] in student_grades.rb
    1: require 'byebug'
    2:
    3: def calculate_average(grades)
    4:   byebug
    5:   total = grades.sum
=>  6:   average = total.to_f / grades.length
    7:   average
    8: end

Check the total variable:

 
(byebug) total
Output
362

You can also inspect method calls before executing them:

 
(byebug) total.to_f / grades.length
Output
90.5

To remove the display expression:

 
(byebug) undisplay 1

The info command shows useful debugging information. View all local variables:

 
(byebug) info locals
Output
grades = [95, 87, 92, 88]
total = 362

These inspection capabilities help you understand exactly what's happening in your code without making assumptions.

When debugging applications with nested method calls, you need to understand not just the current method but also how execution arrived there. Byebug's stack navigation commands let you examine different levels of the call stack.

Create a script with a deeper call hierarchy:

order_processor.rb
require 'byebug'

def calculate_tax(subtotal, tax_rate)
  tax = subtotal * tax_rate
  tax
end

def calculate_shipping(weight)
  base_rate = 5.0
  weight_charge = weight * 0.5
  base_rate + weight_charge
end

def calculate_total(items, tax_rate)
  subtotal = items.sum { |item| item[:price] * item[:quantity] }
  total_weight = items.sum { |item| item[:weight] * item[:quantity] }

  byebug

  tax = calculate_tax(subtotal, tax_rate)
  shipping = calculate_shipping(total_weight)

  subtotal + tax + shipping
end

def process_order(order)
  total = calculate_total(order[:items], order[:tax_rate])
  puts "Order total: $#{total.round(2)}"
end

order = {
  items: [
    { name: 'Book', price: 15.99, quantity: 2, weight: 1.5 },
    { name: 'Pen', price: 2.50, quantity: 5, weight: 0.1 }
  ],
  tax_rate: 0.08
}

process_order(order)

Run the script:

 
ruby order_processor.rb

Execution pauses in the calculate_total method:

Output
[13, 22] in order_processor.rb
   15:   subtotal = items.sum { |item| item[:price] * item[:quantity] }
   16:   total_weight = items.sum { |item| item[:weight] * item[:quantity] }
   17: 
   18:   byebug
   19: 
=> 20:   tax = calculate_tax(subtotal, tax_rate)
   21:   shipping = calculate_shipping(total_weight)
   22: 
   23:   subtotal + tax + shipping
   24: end
(byebug)

Check the calculated values:

 
(byebug) subtotal
Output
44.48
 
(byebug) total_weight
Output
3.5

The backtrace command shows the complete call stack:

 
(byebug) backtrace
Output
--> #0  Object.calculate_total(items#Hash, tax_rate#Float) at order_processor.rb:19
    #1  Object.process_order(order#Hash) at order_processor.rb:27
    #2  <top (required)> at order_processor.rb:40

The up command moves up one level in the call stack:

 
(byebug) up
Output
[22, 31] in order_processor.rb
   22: 
   23:   subtotal + tax + shipping
   24: end
   25: 
   26: def process_order(order)
=> 27:   total = calculate_total(order[:items], order[:tax_rate])
   28:   puts "Order total: $#{total.round(2)}"
   29: end
   30: 
   31: order = {

You're now in the process_order method's frame. Access its variables:

 
(byebug) order
Output
{items: [{name: "Book", price: 15.99, quantity: 2, weight: 1.5}, {name: "Pen", price: 2.5, quantity: 5, weight: 0.1}], tax_rate: 0.08}

Move up another level:

 
(byebug) up
Output
[35, 44] in order_processor.rb
   35:   tax_rate: 0.08
   36: }
   37:
   38: process_order(order)
   39:
=> 40: <top (required)> at order_processor.rb:40

The down command moves back down the stack:

 
(byebug) down
Output
   22: 
   23:   subtotal + tax + shipping
   24: end
   25: 
   26: def process_order(order)
=> 27:   total = calculate_total(order[:items], order[:tax_rate])
   28:   puts "Order total: $#{total.round(2)}"
   29: end
   30: 
   31: order = {

To see which frame you're currently in:

 
(byebug) where
Output
--> #0  Object.calculate_total(items#Hash, tax_rate#Float) at order_processor.rb:19
    #1  Object.process_order(order#Hash) at order_processor.rb:27
    #2  <top (required)> at order_processor.rb:40

This navigation capability helps you understand the execution context and trace how data flows through your application.

Controlling execution with step commands

Byebug provides fine-grained control over how your code executes. You can step into methods to see their internal workings or skip over them when you're confident they work correctly.

Add a breakpoint to your order processor to demonstrate stepping:

order_processor.rb
require 'byebug'

def calculate_tax(subtotal, tax_rate)
  ...
end

def calculate_shipping(weight)
  ...
end

def calculate_total(items, tax_rate)
  subtotal = items.sum { |item| item[:price] * item[:quantity] }
  total_weight = items.sum { |item| item[:weight] * item[:quantity] }

byebug
tax = calculate_tax(subtotal, tax_rate) shipping = calculate_shipping(total_weight) subtotal + tax + shipping end def process_order(order) ... end order = { ... } process_order(order)

Run the script:

 
ruby order_processor.rb

At the breakpoint, use next to move to the next line:

Output
   15:   subtotal = items.sum { |item| item[:price] * item[:quantity] }
   16:   total_weight = items.sum { |item| item[:weight] * item[:quantity] }
   17: 
   18:   byebug
   19: 
=> 20:   tax = calculate_tax(subtotal, tax_rate)
   21:   shipping = calculate_shipping(total_weight)
   22: 
   23:   subtotal + tax + shipping
   24: end
(byebug) next

[16, 25] in /Users/stanley/ruby_byebug_debugging/order_processor.rb
   16:   total_weight = items.sum { |item| item[:weight] * item[:quantity] }
   17: 
   18:   byebug
   19: 
   20:   tax = calculate_tax(subtotal, tax_rate)
=> 21:   shipping = calculate_shipping(total_weight)
   22: 
   23:   subtotal + tax + shipping
   24: end
   25: 
(byebug) 

The step command enters the method call:

 
(byebug) step
Output
[1, 10] in order_processor.rb
    8: def calculate_shipping(weight)
=>  9:   base_rate = 5.0
   10:   weight_charge = weight * 0.5
   11:   base_rate + weight_charge
   12: end
   13: 

You're now inside calculate_tax. Check the parameters:

 
(byebug) subtotal
Output
44.48
 
(byebug) tax_rate
Output
0.08

Execute the calculation:

 
(byebug) next
Output
[1, 10] in order_processor.rb
    1: require 'byebug'
    2: 
    3: def calculate_tax(subtotal, tax_rate)
    4:   tax = subtotal * tax_rate
=>  5:   tax
    6: end

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

 
(byebug) finish
Output
[13, 22] in order_processor.rb
   13: end
   14: 
   15: def calculate_total(items, tax_rate)
   16:   subtotal = items.sum { |item| item[:price] * item[:quantity] }
   17:   total_weight = items.sum { |item| item[:weight] * item[:quantity] }
   18:   
   19:   byebug
   20:   
   21:   tax = calculate_tax(subtotal, tax_rate)
=> 22:   shipping = calculate_shipping(total_weight)

You're back in calculate_total. Check the tax value:

 
(byebug) tax
Output
3.5584

These stepping commands give you complete control over execution flow, allowing you to dive deep into problematic code or skip past sections you trust.

Final thoughts

This article has shown how Byebug simplifies the process of debugging Ruby applications by giving you complete control over code execution. With its ability to pause programs at specific points, inspect variable states, and step through logic line by line, Byebug turns debugging into a structured, efficient workflow.

By learning to navigate the call stack, evaluate expressions, and use commands like next, step, and finish, you can pinpoint issues quickly and understand how your code behaves in detail.

Ultimately, Byebug equips you with the tools to move beyond guesswork, helping you trace, analyze, and fix bugs more effectively while improving your overall Ruby development workflow.

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.