CrackedRuby CrackedRuby

Breakpoints and Watchpoints

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