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:
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:
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
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:
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:
[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:
- It displays the code around your current location with line numbers
- An arrow
=>marks the next line to execute - The
(byebug)prompt appears, waiting for your commands - You have access to all variables in the current scope
Check the value of the celsius parameter:
(byebug) celsius
0
Byebug provides several essential commands for controlling execution:
continueorc: Resume execution until the next breakpoint or program endnextorn: Execute the current line and move to the next linestepors: Step into method callsfinishorfin: Execute until the current method returnsquitorq: Exit the debugger and terminate the programhelp: Display available commands
Execute the current line and move forward:
(byebug) next
[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
32.0
Continue execution to hit the breakpoint again for the next temperature:
(byebug) continue
[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
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:
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:
[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
4
(byebug) grades.max
95
(byebug) grades.min
87
The display command automatically shows an expression's value after each step:
(byebug) display grades.sum
1: grades.sum = 362
Now execute the next line:
(byebug) next
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
362
You can also inspect method calls before executing them:
(byebug) total.to_f / grades.length
90.5
To remove the display expression:
(byebug) undisplay 1
The info command shows useful debugging information. View all local variables:
(byebug) info locals
grades = [95, 87, 92, 88]
total = 362
These inspection capabilities help you understand exactly what's happening in your code without making assumptions.
Navigating the call stack
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:
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:
[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
44.48
(byebug) total_weight
3.5
The backtrace command shows the complete call stack:
(byebug) backtrace
--> #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
[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
{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
[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
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
--> #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:
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:
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
[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
44.48
(byebug) tax_rate
0.08
Execute the calculation:
(byebug) next
[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
[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
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.