Overview
Breakpoints and watchpoints are debugging mechanisms that control program execution flow during development and troubleshooting. A breakpoint suspends program execution at a specified line of code, method entry, or condition, allowing inspection of program state at that moment. A watchpoint monitors specific variables or memory locations and triggers execution suspension when their values change or meet defined conditions.
Debuggers implement these mechanisms by modifying the program's execution environment. For breakpoints, the debugger typically replaces the target instruction with a trap instruction or maintains a table of breakpoint locations that the runtime checks. For watchpoints, the debugger monitors memory addresses or variable bindings, comparing values between instruction cycles to detect changes.
These tools originated in low-level debugging of compiled languages where examining register contents and memory was essential. Modern high-level language debuggers adapted these concepts for source-level debugging, mapping breakpoints to source lines rather than memory addresses and watchpoints to variable names rather than raw memory locations.
# Program execution normally flows continuously
def calculate_total(items)
subtotal = items.sum(&:price)
tax = subtotal * 0.08
total = subtotal + tax
total.round(2)
end
# With a breakpoint at line 3, execution pauses before calculating tax
# allowing inspection of subtotal value before tax calculation proceeds
The distinction between breakpoints and watchpoints reflects different debugging needs. Breakpoints answer "what is the program state at this location?" while watchpoints answer "when and where does this value change?" Both mechanisms reduce the iteration time in debugging workflows by eliminating manual print statement insertion and recompilation cycles.
Key Principles
Execution Suspension
Debuggers suspend program execution by interrupting the normal instruction cycle. When execution reaches a breakpoint location, the debugger's runtime intercepts control before executing the next instruction. The program remains in a suspended state, maintaining all variable values, call stack information, and thread states. This suspension persists until the developer issues a command to resume execution, step to the next instruction, or terminate the debugging session.
Breakpoint Types
Line breakpoints stop execution when control reaches a specific source code line. The debugger maps source lines to executable instructions, handling the complexity that one source line may compile to multiple machine instructions or that optimizations may reorder operations.
Method breakpoints trigger when a method is invoked, regardless of where the call originates. This addresses scenarios where a method is called from multiple locations and the developer needs to inspect each invocation.
Conditional breakpoints include an expression that must evaluate to true for the breakpoint to trigger. The debugger evaluates the condition when execution reaches the breakpoint location and only suspends if the condition is satisfied. This reduces the number of unwanted suspension events in loops or frequently called methods.
Exception breakpoints suspend execution when specific exception classes are raised, before normal exception handling propagates the error up the call stack. This captures the exact state when the error occurred, before any rescue blocks or ensure clauses modify program state.
# Line breakpoint at line 4 stops regardless of array contents
def process_data(array)
result = []
array.each do |item|
transformed = item * 2 # Breakpoint here stops on every iteration
result << transformed
end
result
end
# Conditional breakpoint: item > 100
# Only suspends when item value exceeds 100, reducing interruptions
Watchpoint Mechanisms
Watchpoints monitor data rather than code locations. A write watchpoint triggers when the debugger detects a change to the monitored location. The debugger compares the value before and after each instruction that might modify the location. Read watchpoints trigger when the program accesses the monitored value, identifying where code reads data. Access watchpoints combine both behaviors, triggering on any interaction with the location.
The granularity of watchpoint monitoring varies. Variable watchpoints monitor named bindings in the source code. Attribute watchpoints monitor object instance variables or properties. Memory watchpoints, more common in systems programming, monitor raw memory addresses regardless of source-level variable names.
State Inspection
During suspension, the debugger provides access to the program's runtime state. The call stack shows the sequence of method invocations leading to the current location. Local variables within each stack frame retain their current values. Object inspection reveals instance variable values and object identity. Thread information shows the state of concurrent execution paths in multi-threaded programs.
Execution Control Commands
After suspension, execution resumes through specific commands. Continue resumes normal execution until the next breakpoint or program termination. Step-over executes the current line and suspends at the next line in the same method, treating method calls as atomic operations. Step-into enters method calls, suspending at the first line of the called method. Step-out completes the current method and suspends at the call site in the calling method.
def outer_method
value = calculate(10) # Breakpoint here
result = process(value)
result * 2
end
def calculate(n)
n * n
end
# Step-over executes calculate(10) atomically, next stop at line 3
# Step-into enters calculate method, next stop at first line of calculate
# Continue resumes until next breakpoint or program end
Ruby Implementation
Ruby's debugging interface integrates with the interpreter's execution model. The standard library includes the debug gem, which provides comprehensive breakpoint and watchpoint functionality. Alternative debuggers like byebug and pry offer different feature sets and interfaces.
Setting Breakpoints
The debug gem uses the debugger statement (or binding.break) to create breakpoints directly in source code. When execution reaches this statement, the debugger activates and suspends execution.
require 'debug'
def process_order(order)
validate_items(order.items)
debugger # Execution suspends here
calculate_total(order)
end
# Alternative syntax
def check_inventory(item)
binding.break # Equivalent to debugger
item.quantity > 0
end
Interactive debugger sessions set breakpoints through commands rather than code modification. The break command accepts file names and line numbers, method names, or class and method combinations.
# In debugger REPL
break app.rb:42 # Line breakpoint
break MyClass#instance_method # Instance method
break MyClass.class_method # Class method
break process_order # Method name without class
Conditional breakpoints include Ruby expressions that determine whether suspension occurs. The debugger evaluates the condition in the context of the current execution frame, accessing local variables and method parameters.
# Conditional breakpoint: only trigger when total exceeds threshold
break app.rb:42 if total > 1000
# Conditional with method call
break calculate_tax if items.count > 10
# Complex condition
break process_data if user.admin? && data.sensitive?
Managing Breakpoints
The info breakpoints command lists all active breakpoints with their identifiers, locations, and conditions. The delete command removes breakpoints by identifier. The disable command temporarily deactivates breakpoints without removing them, and enable reactivates disabled breakpoints.
# Debugger session
info breakpoints
# => 1 app.rb:42 enabled if total > 1000
# => 2 app.rb:67 enabled
# => 3 lib/model.rb:23 disabled
delete 2 # Remove breakpoint 2
disable 1 # Deactivate breakpoint 1
enable 3 # Activate breakpoint 3
Watchpoints in Ruby
Ruby debuggers implement watchpoints through the watch command, monitoring instance variables, local variables, or expressions. The debugger checks the watched expression after each step operation, suspending when the value changes.
class Account
def initialize(balance)
@balance = balance
end
def withdraw(amount)
debugger
@balance -= amount # Watch @balance to see when it changes
end
end
# In debugger
watch @balance # Monitor instance variable
watch local_var # Monitor local variable
watch items.count # Monitor expression
TracePoint API
Ruby's TracePoint API provides programmatic access to execution events, enabling custom debugger implementations. TracePoint objects register callbacks for specific events like method calls, method returns, line execution, and exception raising.
require 'set'
class SimpleDebugger
def initialize
@breakpoints = Set.new
@trace = nil
end
def add_breakpoint(file, line)
@breakpoints.add([file, line])
end
def start
@trace = TracePoint.new(:line) do |tp|
if @breakpoints.include?([tp.path, tp.lineno])
puts "Breakpoint hit: #{tp.path}:#{tp.lineno}"
puts "Local variables: #{tp.binding.local_variables}"
binding.irb # Drop into REPL
end
end
@trace.enable
end
def stop
@trace.disable if @trace
end
end
# Usage
debugger = SimpleDebugger.new
debugger.add_breakpoint(__FILE__, 35)
debugger.start
Remote Debugging
Ruby supports remote debugging through debug adapters that communicate over TCP sockets. The ruby-debug-ide gem implements the Debug Adapter Protocol, allowing IDEs to control debugging sessions remotely.
# Start program with remote debugging
ruby -r debug/open --rdbg-opt-host 0.0.0.0 --rdbg-opt-port 12345 app.rb
# Or within code
require 'debug/open'
DEBUGGER__.open(host: '0.0.0.0', port: 12345)
# IDE connects to port 12345 and controls debugging session
Practical Examples
Debugging Loop Iterations
A method processes array elements but produces incorrect results for specific inputs. A conditional breakpoint isolates the problematic iteration without stopping on every loop pass.
def calculate_discounts(orders)
orders.map do |order|
base_price = order.total
discount = if order.customer.vip?
base_price * 0.15
elsif order.total > 500
base_price * 0.10
else
0
end
base_price - discount
end
end
# Set breakpoint with condition
# break calculate_discounts if order.total > 500 && !order.customer.vip?
# Execution suspends only for non-VIP orders over $500
# Inspect: order.total, order.customer.vip?, discount
# Reveals incorrect discount calculation logic
Tracking State Changes
An object's internal state changes unexpectedly during execution. Watchpoints identify exactly where and when the modification occurs.
class ShoppingCart
attr_reader :items
def initialize
@items = []
@total = 0
end
def add_item(item)
@items << item
update_total
end
def remove_item(item)
@items.delete(item)
update_total
end
def apply_coupon(code)
@coupon = code
update_total
end
private
def update_total
@total = @items.sum(&:price)
@total *= 0.9 if @coupon
end
end
# Watch @total to find unexpected modifications
# watch @total
#
# Execution reveals @total changes in update_total method
# Both add_item and apply_coupon call update_total
# Watchpoint shows exact call stack when @total changes
Exception Debugging
An application raises exceptions intermittently. Exception breakpoints capture the error state before exception handling code runs.
class DataProcessor
def process(records)
records.each do |record|
validate_record(record)
transform_record(record)
save_record(record)
end
end
private
def validate_record(record)
raise ArgumentError, "Invalid record" unless record[:id]
end
def transform_record(record)
record[:value] = record[:value].to_i * 2
end
def save_record(record)
Database.insert(record)
end
end
# Set exception breakpoint
# catch ArgumentError
#
# Execution suspends when ArgumentError is raised in validate_record
# Inspect record contents at exact moment of error
# Call stack shows which record in the batch caused the failure
Multi-threaded Debugging
Concurrent code exhibits race conditions. Breakpoints in specific threads isolate problematic execution sequences.
require 'thread'
class Counter
def initialize
@value = 0
@mutex = Mutex.new
end
def increment
@mutex.synchronize do
temp = @value
sleep(0.001) # Simulate work
@value = temp + 1
end
end
def value
@mutex.synchronize { @value }
end
end
counter = Counter.new
threads = 10.times.map do
Thread.new do
100.times { counter.increment }
end
end
# Set breakpoint in increment method
# break Counter#increment
#
# Inspect Thread.current, @value, @mutex.locked?
# Step through synchronize block
# Verify mutex prevents concurrent access to @value
Method Call Tracing
A method receives incorrect arguments from an unknown call site. Method breakpoints capture every invocation with full call stack context.
class PaymentProcessor
def charge(amount, currency)
raise ArgumentError, "Invalid amount" if amount <= 0
raise ArgumentError, "Invalid currency" unless %w[USD EUR GBP].include?(currency)
process_charge(amount, currency)
end
private
def process_charge(amount, currency)
# Implementation
end
end
# Set method breakpoint
# break PaymentProcessor#charge
#
# Execution suspends on every charge call
# Inspect amount, currency parameters
# Examine call stack with 'backtrace' to find caller
# Discover unexpected call from OrderController#complete
Common Patterns
Breakpoint Initialization Pattern
Place temporary breakpoints at method entry points to verify argument values and initial state before algorithm execution.
def complex_calculation(data, options = {})
debugger # Verify data structure and options before processing
threshold = options[:threshold] || 100
results = []
data.each do |item|
score = calculate_score(item)
results << item if score > threshold
end
results.sort_by { |item| -item.score }
end
# Remove debugger statement after verification
Conditional Break Pattern
Combine breakpoints with conditionals that match specific runtime states to avoid irrelevant suspensions in high-frequency code paths.
def process_transaction(transaction)
# break if transaction.amount > 10000 && transaction.type == 'withdrawal'
validate_transaction(transaction)
execute_transaction(transaction)
log_transaction(transaction)
end
# Breakpoint only triggers for large withdrawals
# Reduces debugging noise from normal transactions
Watch and Verify Pattern
Set watchpoints on critical state variables during operations that should preserve invariants, detecting unexpected modifications.
class BankAccount
def transfer_to(other_account, amount)
# watch @balance
# Expected: @balance decreases by exactly amount
raise InsufficientFunds if @balance < amount
@balance -= amount
other_account.deposit(amount)
# Watchpoint confirms @balance changed by correct amount
end
end
Stack Frame Navigation Pattern
When execution suspends at a breakpoint, navigate up the call stack to examine parent contexts and understand how the current state was reached.
def controller_action
user = current_user
data = fetch_data(user)
result = process_data(data)
render json: result
end
def process_data(data)
transformed = transform(data)
validated = validate(transformed)
validated
end
def validate(data)
debugger # Suspended here
# Command: up (move to process_data frame)
# Command: up (move to controller_action frame)
# Inspect user, data in parent contexts
# Command: down (return to validate frame)
data.all? { |item| item.valid? }
end
Post-Mortem Debugging Pattern
Set exception breakpoints to capture program state immediately when errors occur, before stack unwinding destroys context.
class DataImporter
def import(file)
records = parse_file(file)
records.each { |record| save_record(record) }
rescue StandardError => e
# Exception breakpoint captures state here
# catch StandardError
# Inspect: e.message, e.backtrace, records, current record
log_error(e)
raise
end
end
Breakpoint Removal Pattern
After fixing issues, remove all inline breakpoint statements before committing code. Use debugger commands for temporary investigation breakpoints rather than code modifications.
# Development version
def calculate(x, y)
debugger # DON'T commit this
result = x * y + 10
result
end
# Production version
def calculate(x, y)
result = x * y + 10
result
end
# Better: Use debugger REPL commands
# break calculate
# No code modification required
Tools & Ecosystem
debug gem
Ruby's standard debugger, included in the standard library starting with Ruby 3.1. Provides comprehensive debugging features including breakpoints, watchpoints, thread control, and remote debugging support.
require 'debug'
# Start debugging session
binding.break
# Or from command line
ruby -r debug app.rb
# Key commands:
# break file:line [condition] - Set breakpoint
# watch expression - Set watchpoint
# catch ExceptionClass - Break on exception
# continue - Resume execution
# step - Step into
# next - Step over
# finish - Step out
# info breakpoints - List breakpoints
# backtrace - Show call stack
byebug
Feature-rich debugger for Ruby 2.4-3.0, popular in Rails applications. Supports breakpoints, stack navigation, and expression evaluation.
gem 'byebug'
# In code
require 'byebug'
byebug # Equivalent to debugger statement
# Commands similar to debug gem with some syntax differences
# break app.rb:42 - Set breakpoint
# condition 1 x > 10 - Add condition to breakpoint 1
# display x - Display x value after each step
# undisplay 1 - Remove display 1
# thread list - Show all threads
# thread switch 2 - Switch to thread 2
pry
Interactive Ruby shell with debugging capabilities. Offers powerful object introspection and modification features during debugging sessions.
gem 'pry'
gem 'pry-byebug' # Adds step, next, continue commands
require 'pry'
def calculate(x)
binding.pry # Drop into Pry session
result = x * 2
result
end
# Pry commands
# whereami - Show current location in code
# ls object - List object methods and variables
# cd object - Change scope to object
# show-source method - Display method source code
# edit method - Open method in editor
# ! - Execute previous command
ruby-debug-ide
Debug Adapter Protocol implementation for Ruby, enabling IDE integration. VSCode, RubyMine, and other IDEs use this for graphical debugging interfaces.
gem 'ruby-debug-ide'
gem 'debase' # Required for Ruby 2.0+
# Start application with remote debugging
rdebug-ide --host 0.0.0.0 --port 1234 -- app.rb
# VSCode launch.json configuration
{
"type": "Ruby",
"request": "attach",
"name": "Attach to rdebug-ide",
"remoteHost": "127.0.0.1",
"remotePort": "1234",
"remoteWorkspaceRoot": "${workspaceRoot}"
}
IDE Integration
Modern IDEs provide graphical debugging interfaces that abstract command-line debugger operations. Set breakpoints by clicking line numbers, view variables in dedicated panels, and control execution with toolbar buttons.
VSCode with the Ruby LSP extension supports inline breakpoints, variable inspection, and watch expressions through a graphical interface. RubyMine includes advanced features like exception breakpoints, method breakpoints, and hit count conditions.
Rails applications benefit from IDE debugging through rack-based applications. The IDE starts the Rails server under debugger control, allowing breakpoints in controllers, models, and views.
Performance Profiling Tools
While not debuggers, profiling tools complement debugging by identifying performance bottlenecks. The stackprof gem samples call stacks to determine where the program spends time. The ruby-prof gem provides detailed execution profiles.
gem 'stackprof'
require 'stackprof'
StackProf.run(mode: :wall, out: 'tmp/stackprof.dump') do
# Code to profile
process_large_dataset(data)
end
# Analyze results
# stackprof tmp/stackprof.dump --text
Common Pitfalls
Forgotten Inline Breakpoints
Committing code with debugger or binding.break statements causes production applications to suspend when those lines execute. This halts request processing and requires manual intervention to continue execution.
# WRONG: Committed to version control
def process_order(order)
debugger # Application suspends in production
validate_order(order)
charge_payment(order)
end
# CORRECT: Remove before commit
def process_order(order)
validate_order(order)
charge_payment(order)
end
# Use git hooks to prevent commits containing debugger statements
# .git/hooks/pre-commit
grep -r "binding.break\|debugger" app/ lib/
if [ $? -eq 0 ]; then
echo "Error: debugger statements found"
exit 1
fi
Ineffective Conditional Breakpoints
Writing conditions that reference unavailable variables or use incorrect scope causes the breakpoint to fail silently or trigger unexpectedly.
class OrderProcessor
def process(orders)
orders.each do |order|
calculate_total(order)
end
end
def calculate_total(order)
# WRONG: 'orders' not in scope, condition never true
# break if orders.count > 10
# CORRECT: Use available variables
# break if order.items.count > 10
order.items.sum(&:price)
end
end
Watchpoint Overhead
Setting watchpoints on frequently modified variables in tight loops causes severe performance degradation. The debugger evaluates the watched expression after every step, creating overhead that makes the program unusable.
# WRONG: Watching variables in hot paths
def process_data(array)
sum = 0
# watch sum - Triggers after every iteration
array.each do |item|
sum += item # Thousands of suspensions
end
sum
end
# CORRECT: Watch less frequently modified state
def process_batches(batches)
results = []
# watch results - Triggers once per batch
batches.each do |batch|
results << process_batch(batch)
end
results
end
Race Conditions in Multi-threaded Code
Debugging multi-threaded code with breakpoints changes timing characteristics, masking or altering race conditions. The suspension introduced by breakpoints may prevent races from manifesting during debugging.
# Race condition exists but breakpoint hides it
class Counter
def initialize
@value = 0
end
def increment
# debugger - Suspension serializes thread access
temp = @value
@value = temp + 1 # Race condition when no breakpoint
end
end
# Use logging or assertions instead
def increment
Thread.current[:counter_trace] = @value
temp = @value
@value = temp + 1
puts "Thread #{Thread.current.object_id}: #{Thread.current[:counter_trace]} -> #{@value}"
end
Stepping Into Framework Code
Using step-into commands in framework-heavy applications leads to deep dives into library code, making it difficult to return to application logic.
def controller_action
@user = User.find(params[:id]) # debugger here
# Step-into enters ActiveRecord internals
# Hundreds of steps through framework code
# Difficult to return to controller context
render json: @user
end
# Use step-over to treat framework calls atomically
# Use method breakpoints on specific application methods
# break MyClass#specific_method
Incorrect Scope Assumptions
Attempting to evaluate expressions in the wrong scope produces confusing results. Debugger commands execute in the current frame's scope, not in parent or child contexts.
class PaymentProcessor
def initialize
@processor = StripeProcessor.new
end
def charge(amount)
validate_amount(amount)
@processor.charge(amount)
end
def validate_amount(amount)
debugger
# @processor available in this frame
# But validate_amount doesn't use @processor
# Checking @processor here may be misleading
raise ArgumentError if amount <= 0
end
end
# Navigate stack frames explicitly
# up - Move to charge method frame
# down - Return to validate_amount frame
Breakpoint Proliferation
Setting numerous breakpoints without organization makes debugging sessions confusing. Execution suspends frequently at unexpected locations, interrupting workflow.
# 20+ breakpoints scattered across codebase
# Execution suspends repeatedly
# Difficult to remember breakpoint purposes
# Debugging session becomes unproductive
# Maintain minimal active breakpoints
# Delete breakpoints after fixing related issues
# info breakpoints - Review active breakpoints regularly
# delete all - Clear all breakpoints when starting new investigation
Reference
Debugger Commands
| Command | Description | Example |
|---|---|---|
| break location [condition] | Set breakpoint at location with optional condition | break app.rb:42 if total > 1000 |
| watch expression | Monitor expression value changes | watch @balance |
| catch ExceptionClass | Break when exception is raised | catch ArgumentError |
| condition number expression | Add condition to existing breakpoint | condition 1 x > 10 |
| delete number | Remove breakpoint by number | delete 1 |
| delete all | Remove all breakpoints | delete all |
| disable number | Temporarily disable breakpoint | disable 1 |
| enable number | Re-enable disabled breakpoint | enable 1 |
| info breakpoints | List all breakpoints | info breakpoints |
| continue | Resume execution until next breakpoint | continue |
| step | Execute next line, enter methods | step |
| next | Execute next line, skip methods | next |
| finish | Complete current method, return to caller | finish |
| backtrace | Display call stack | backtrace |
| up | Move to parent stack frame | up |
| down | Move to child stack frame | down |
| frame number | Jump to specific stack frame | frame 3 |
| eval expression | Evaluate Ruby expression | eval user.admin? |
| display expression | Show expression value after each step | display total |
| undisplay number | Remove display expression | undisplay 1 |
| info threads | List all threads | info threads |
| thread number | Switch to specific thread | thread 2 |
Breakpoint Locations
| Location Format | Matches | Example |
|---|---|---|
| file:line | Specific line in file | app.rb:42 |
| file:line:column | Specific column position | app.rb:42:10 |
| method_name | Any method with name | calculate_total |
| ClassName#method | Instance method | Order#calculate_total |
| ClassName.method | Class method | Order.create_from_json |
| ClassName | First line of class definition | Order |
Watchpoint Types
| Type | Triggers When | Use Case |
|---|---|---|
| watch variable | Variable value changes | Track unexpected modifications |
| watch expression | Expression result changes | Monitor complex conditions |
| watch @instance_var | Instance variable changes | Debug object state mutations |
| watch local_var | Local variable changes | Track algorithm state |
Execution Control
| Control | Effect | Common Use |
|---|---|---|
| continue (c) | Run until next breakpoint or end | Complete initialization, reach error location |
| step (s) | Execute one line, enter methods | Inspect method internals |
| next (n) | Execute one line, skip methods | Stay at current abstraction level |
| finish (fin) | Complete current method | Exit utility method, return to business logic |
| until location | Run until location reached | Skip to end of loop |
| return value | Exit method with specified return value | Test error handling, modify flow |
Stack Navigation
| Command | Direction | Result |
|---|---|---|
| backtrace (bt) | None | Display full call stack |
| up | Toward caller | Move to parent frame |
| down | Toward callee | Move to child frame |
| frame N | Direct jump | Switch to frame N |
| where | None | Show current position in stack |
Information Commands
| Command | Output | Purpose |
|---|---|---|
| info breakpoints | All breakpoints with IDs and conditions | Review debugging setup |
| info threads | Thread list with states | Debug concurrency |
| info display | Active display expressions | Check watched expressions |
| info locals | Local variables in current frame | Inspect method state |
| info instance_variables | Instance variables of current object | Examine object state |
| info global_variables | All global variables | Check global state |
Common Conditional Expressions
| Pattern | Condition | Purpose |
|---|---|---|
| Value threshold | x > 100 | Catch edge cases |
| Type check | item.is_a?(SpecialCase) | Debug polymorphic code |
| State validation | !order.valid? | Find invalid states |
| Collection size | items.count > 10 | Debug batch processing |
| Complex logic | user.admin? && resource.sensitive? | Specific scenarios |
| Method result | calculate_risk(data) > 0.8 | Algorithm behavior |
| Nil check | value.nil? | Find nil errors |