# Debug Ruby Applications with Byebug

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](https://github.com/deivid-rodriguez/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.

[ad-logs]

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

```command
mkdir ruby_byebug_debugging && cd ruby_byebug_debugging
```

Initialize a new bundle to manage dependencies:

```command
bundle init
```

Open the generated `Gemfile` and add Byebug:

```ruby
[label Gemfile]
source 'https://rubygems.org'

[highlight]
gem 'byebug'
[/highlight]
```

Install the gem:

```command
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`:

```ruby
[label 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:

```command
ruby temperature_converter.rb
```

```text
[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:

```ruby
[label temperature_converter.rb]
[highlight]
require 'byebug'

[/highlight]
def celsius_to_fahrenheit(celsius)
[highlight]
  byebug
[/highlight]
  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:

```command
ruby temperature_converter.rb
```

Execution stops at the `byebug` statement:

```text
[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:

```text
(byebug) celsius
```

```text
[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:

```text
(byebug) next
```

```text
[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:

```text
(byebug) fahrenheit
```

```text
[output]
32.0
```

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

```text
(byebug) continue
```

```text
[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:

```text
(byebug) celsius
```

```text
[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:

```ruby
[label 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:

```command
ruby student_grades.rb
```

When execution pauses, you can inspect the `grades` array:

```text
[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:

```text
(byebug) grades.length
```

```text
[output]
4
```

```text
(byebug) grades.max
```

```text
[output]
95
```

```text
(byebug) grades.min
```

```text
[output]
87
```

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

```text
(byebug) display grades.sum
```

```text
[output]
1: grades.sum = 362
```

Now execute the next line:

```text
(byebug) next
```

```text
[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:

```text
(byebug) total
```

```text
[output]
362
```

You can also inspect method calls before executing them:

```text
(byebug) total.to_f / grades.length
```

```text
[output]
90.5
```

To remove the display expression:

```text
(byebug) undisplay 1
```

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

```text
(byebug) info locals
```

```text
[output]
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:

```ruby
[label 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:

```command
ruby order_processor.rb
```

Execution pauses in the `calculate_total` method:

```text
[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:

```text
(byebug) subtotal
```

```text
[output]
44.48
```

```text
(byebug) total_weight
```

```text
[output]
3.5
```

The `backtrace` command shows the complete call stack:

```text
(byebug) backtrace
```

```text
[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:

```text
(byebug) up
```

```text
[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:

```text
(byebug) order
```

```text
[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:

```text
(byebug) up
```

```text
[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:

```text
(byebug) down
```

```text
[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:

```text
(byebug) where
```

```text
[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:

```ruby
[label 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] }
  
[highlight]
  byebug
  
[/highlight]
  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:

```command
ruby order_processor.rb
```

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

```text
[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:

```text
(byebug) step
```

```text
[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:

```text
(byebug) subtotal
```

```text
[output]
44.48
```

```text
(byebug) tax_rate
```

```text
[output]
0.08
```

Execute the calculation:

```text
(byebug) next
```

```text
[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:

```text
(byebug) finish
```

```text
[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:

```text
(byebug) tax
```

```text
[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.
