CrackedRuby logo

CrackedRuby

Exception Backtrace

Overview

Exception backtrace represents the sequence of method calls that led to an exception in Ruby. Ruby generates backtrace information automatically when exceptions occur, storing the call stack as an array of strings that developers can access programmatically. The backtrace system operates through several key classes: Exception, Thread::Backtrace, and Thread::Backtrace::Location.

Ruby stores backtrace data in two primary forms. The traditional format returns an array of strings containing file paths, line numbers, and method names. The newer location-based format provides structured objects with detailed information about each stack frame. Both formats offer different advantages for debugging and error reporting.

begin
  raise "Something went wrong"
rescue => e
  puts e.backtrace.first
  # => "example.rb:2:in `<main>'"
end

The backtrace system integrates with Ruby's exception hierarchy. Every exception object carries backtrace information from the point where the exception was raised. Ruby captures this information automatically, including method names, file paths, line numbers, and block context.

def outer_method
  inner_method
end

def inner_method
  raise StandardError, "Error in inner method"
end

begin
  outer_method
rescue => e
  e.backtrace.each { |frame| puts frame }
end
# => example.rb:6:in `inner_method'
# => example.rb:2:in `outer_method'
# => example.rb:10:in `<main>'

Ruby provides multiple methods for accessing backtrace information. The #backtrace method returns the traditional string array format, while #backtrace_locations returns an array of Thread::Backtrace::Location objects with structured access to frame details.

Basic Usage

Ruby exceptions automatically capture backtrace information when raised. The #backtrace method returns an array of strings representing the call stack, with each string containing the filename, line number, and method name where the call occurred.

def method_a
  method_b
end

def method_b  
  method_c
end

def method_c
  raise "Error occurred"
end

begin
  method_a
rescue => e
  puts "Exception: #{e.message}"
  puts "Backtrace:"
  e.backtrace.each_with_index do |frame, index|
    puts "  #{index}: #{frame}"
  end
end

The #backtrace_locations method provides structured access to stack frame information. Each location object offers methods for extracting specific components of the stack frame without string parsing.

begin
  eval("raise 'Dynamic error'", binding, "dynamic_file.rb", 42)
rescue => e
  location = e.backtrace_locations.first
  puts "File: #{location.path}"
  puts "Line: #{location.lineno}" 
  puts "Method: #{location.label}"
  puts "Absolute path: #{location.absolute_path}"
end
# => File: dynamic_file.rb
# => Line: 42  
# => Method: <main>
# => Absolute path: dynamic_file.rb

Ruby allows manual backtrace manipulation through the #set_backtrace method. This method replaces the exception's backtrace with a custom array of strings, enabling backtrace filtering or modification for specific error handling scenarios.

begin
  raise "Original error"
rescue => e
  # Create custom backtrace
  custom_trace = [
    "custom.rb:1:in `custom_method'",
    "custom.rb:5:in `<main>'"
  ]
  e.set_backtrace(custom_trace)
  puts e.backtrace
end

The caller method generates backtrace information for the current execution point without raising an exception. This method accepts optional parameters to limit the number of frames returned and skip frames from the top of the stack.

def show_caller_info
  puts "Current call stack:"
  caller(0, 3).each_with_index do |frame, index|
    puts "  #{index}: #{frame}"
  end
end

def calling_method
  show_caller_info  
end

calling_method
# => Current call stack:
# =>   0: example.rb:8:in `show_caller_info'
# =>   1: example.rb:12:in `calling_method' 
# =>   2: example.rb:15:in `<main>'

Error Handling & Debugging

Exception backtrace analysis requires understanding Ruby's stack frame format and common patterns that appear in production applications. Stack frames follow the pattern filename:line_number:in 'method_name', with variations for different execution contexts like blocks, class definitions, and eval statements.

Backtrace filtering removes irrelevant frames from stack traces, focusing attention on application-specific code rather than framework or library internals. Ruby applications typically filter frames based on file paths, method names, or gem boundaries.

class BacktraceFilter
  FRAMEWORK_PATTERNS = [
    %r{/gems/},
    %r{/ruby/\d+\.\d+\.\d+/},
    %r{<internal:},
    %r{/lib/ruby/}
  ].freeze

  def self.filter_application_frames(backtrace)
    return [] unless backtrace
    
    backtrace.reject do |frame|
      FRAMEWORK_PATTERNS.any? { |pattern| frame.match?(pattern) }
    end
  end

  def self.extract_error_context(exception, context_lines: 3)
    location = exception.backtrace_locations&.first
    return nil unless location&.path && File.exist?(location.path)

    file_lines = File.readlines(location.path)
    error_line = location.lineno - 1
    
    start_line = [error_line - context_lines, 0].max
    end_line = [error_line + context_lines, file_lines.length - 1].min
    
    {
      file: location.path,
      error_line: location.lineno,
      context: file_lines[start_line..end_line].map.with_index(start_line + 1) do |line, num|
        marker = num == location.lineno ? ">>>" : "   "
        "#{marker} #{num.to_s.rjust(3)}: #{line.chomp}"
      end
    }
  end
end

# Usage example
begin
  JSON.parse("invalid json")
rescue JSON::ParserError => e
  filtered = BacktraceFilter.filter_application_frames(e.backtrace)
  context = BacktraceFilter.extract_error_context(e)
  
  puts "Filtered backtrace:"
  filtered.each { |frame| puts "  #{frame}" }
  
  if context
    puts "\nError context:"
    context[:context].each { |line| puts line }
  end
end

Backtrace analysis for debugging involves examining method call patterns, identifying recursion issues, and understanding execution flow. Ruby's backtrace system provides sufficient information to trace complex execution paths and identify problematic code sections.

class RecursionDetector
  def self.analyze_backtrace(backtrace)
    method_counts = Hash.new(0)
    call_patterns = []
    
    backtrace.each do |frame|
      if match = frame.match(%r{:in `([^']+)'})
        method_name = match[1]
        method_counts[method_name] += 1
        call_patterns << method_name
      end
    end
    
    # Detect potential infinite recursion
    recursive_methods = method_counts.select { |_, count| count > 5 }
    
    # Detect call loops
    pattern_string = call_patterns.join(" -> ")
    loops = detect_loops(call_patterns)
    
    {
      method_counts: method_counts,
      recursive_methods: recursive_methods,
      call_pattern: pattern_string,
      detected_loops: loops,
      stack_depth: backtrace.length
    }
  end
  
  private
  
  def self.detect_loops(calls)
    loops = []
    (2..calls.length/2).each do |pattern_length|
      calls.each_cons(pattern_length * 2) do |sequence|
        first_half = sequence[0, pattern_length]
        second_half = sequence[pattern_length, pattern_length]
        if first_half == second_half
          loops << first_half
          break
        end
      end
    end
    loops.uniq
  end
end

# Example usage for debugging
begin
  # Simulate problematic recursion
  def recursive_method(depth)
    return if depth > 100
    another_method(depth + 1)
  end
  
  def another_method(depth)
    recursive_method(depth)
  end
  
  recursive_method(0)
rescue SystemStackError => e
  analysis = RecursionDetector.analyze_backtrace(e.backtrace)
  puts "Stack analysis:"
  puts "  Total depth: #{analysis[:stack_depth]}"
  puts "  Recursive methods: #{analysis[:recursive_methods]}"
  puts "  Detected loops: #{analysis[:detected_loops]}"
end

Exception chaining and cause tracking help maintain error context through multiple exception handling layers. Ruby's exception system supports cause chaining, where exceptions can reference the original exception that triggered the current error condition.

class ErrorChainAnalyzer
  def self.analyze_exception_chain(exception)
    chain = []
    current = exception
    
    while current
      location = current.backtrace_locations&.first
      chain << {
        exception: current,
        message: current.message,
        class: current.class.name,
        file: location&.path,
        line: location&.lineno,
        method: location&.label
      }
      current = current.cause
    end
    
    chain
  end
  
  def self.format_exception_chain(exception)
    chain = analyze_exception_chain(exception)
    output = []
    
    chain.each_with_index do |link, index|
      prefix = index == 0 ? "Error" : "Caused by"
      location_info = "#{link[:file]}:#{link[:line]}"
      output << "#{prefix}: #{link[:class]}: #{link[:message]}"
      output << "  at #{location_info} in '#{link[:method]}'"
      output << ""
    end
    
    output.join("\n")
  end
end

Production Patterns

Production applications require robust backtrace handling for error reporting, monitoring, and debugging. Backtrace information forms the foundation of error tracking systems, providing developers with context needed to reproduce and fix issues in production environments.

Error reporting systems aggregate backtrace data to identify common failure patterns and prioritize bug fixes. Production backtrace handling involves sanitizing sensitive information, grouping similar errors, and providing sufficient context for remote debugging without exposing application internals.

class ProductionErrorReporter
  SENSITIVE_PATTERNS = [
    /password[=:]\s*\S+/i,
    /token[=:]\s*\S+/i,
    /secret[=:]\s*\S+/i,
    /api[_-]?key[=:]\s*\S+/i
  ].freeze

  def self.sanitize_backtrace(backtrace)
    return [] unless backtrace
    
    backtrace.map do |frame|
      sanitized = frame.dup
      SENSITIVE_PATTERNS.each do |pattern|
        sanitized.gsub!(pattern, '\1[FILTERED]')
      end
      sanitized
    end
  end

  def self.generate_error_fingerprint(exception)
    # Create consistent hash for grouping similar errors
    key_frames = exception.backtrace&.first(5) || []
    fingerprint_data = [
      exception.class.name,
      key_frames.map { |frame| frame.split(':in').first }
    ].flatten.compact
    
    Digest::SHA256.hexdigest(fingerprint_data.join('|'))[0, 16]
  end

  def self.capture_environment_context
    {
      ruby_version: RUBY_VERSION,
      ruby_platform: RUBY_PLATFORM,
      process_id: Process.pid,
      thread_count: Thread.list.count,
      memory_usage: get_memory_usage,
      load_average: get_load_average,
      timestamp: Time.now.utc.iso8601
    }
  end

  def self.report_error(exception, context: {})
    error_report = {
      fingerprint: generate_error_fingerprint(exception),
      exception_class: exception.class.name,
      message: exception.message,
      backtrace: sanitize_backtrace(exception.backtrace),
      cause_chain: build_cause_chain(exception),
      environment: capture_environment_context,
      custom_context: context,
      occurred_at: Time.now.utc.iso8601
    }
    
    # Send to monitoring service
    send_to_monitoring_service(error_report)
    error_report
  end

  private

  def self.build_cause_chain(exception)
    chain = []
    current = exception.cause
    
    while current && chain.length < 10
      chain << {
        class: current.class.name,
        message: current.message,
        backtrace: sanitize_backtrace(current.backtrace&.first(3))
      }
      current = current.cause
    end
    
    chain
  end

  def self.get_memory_usage
    # Platform-specific memory usage detection
    if RUBY_PLATFORM.match?(/darwin/)
      `ps -o rss= -p #{Process.pid}`.strip.to_i * 1024
    elsif RUBY_PLATFORM.match?(/linux/)
      File.read("/proc/#{Process.pid}/status")
          .match(/VmRSS:\s*(\d+)\s*kB/)[1].to_i * 1024 rescue nil
    else
      nil
    end
  end

  def self.get_load_average
    File.read('/proc/loadavg').split.first.to_f rescue nil
  end

  def self.send_to_monitoring_service(report)
    # Implementation depends on monitoring service
    puts "Sending error report: #{report[:fingerprint]}"
  end
end

Web application integration requires backtrace handling that works with request-response cycles and provides meaningful error pages. Rails and other frameworks build upon Ruby's backtrace system to provide developer-friendly error information during development and structured error reporting in production.

class WebErrorHandler
  def initialize(app)
    @app = app
  end

  def call(env)
    begin
      @app.call(env)
    rescue => exception
      handle_web_exception(exception, env)
    end
  end

  private

  def handle_web_exception(exception, env)
    request_context = extract_request_context(env)
    
    # Generate detailed error report
    error_report = {
      exception: exception,
      request: request_context,
      backtrace_analysis: analyze_web_backtrace(exception),
      user_context: extract_user_context(env)
    }

    if development_mode?
      render_developer_error_page(error_report)
    else
      log_production_error(error_report)
      render_user_error_page(exception.class)
    end
  end

  def analyze_web_backtrace(exception)
    return {} unless exception.backtrace

    application_frames = exception.backtrace.select do |frame|
      frame.include?(Rails.root.to_s) if defined?(Rails)
    end

    controller_frames = application_frames.select do |frame|
      frame.match?(/controllers?\//)
    end

    model_frames = application_frames.select do |frame|  
      frame.match?(/models?\//)
    end

    {
      total_frames: exception.backtrace.length,
      application_frames: application_frames.length,
      controller_frames: controller_frames,
      model_frames: model_frames,
      deepest_application_frame: application_frames.first
    }
  end

  def extract_request_context(env)
    {
      method: env['REQUEST_METHOD'],
      path: env['PATH_INFO'],
      query_string: env['QUERY_STRING'],
      user_agent: env['HTTP_USER_AGENT'],
      remote_ip: env['REMOTE_ADDR'],
      session_id: extract_session_id(env),
      request_id: env['action_dispatch.request_id']
    }
  end

  def development_mode?
    ENV['RAILS_ENV'] == 'development' || ENV['RACK_ENV'] == 'development'
  end
end

Structured logging integration captures backtrace information in machine-readable formats for analysis and alerting. Production logging systems process backtrace data to identify trends, measure error rates, and trigger automated responses to critical failures.

class StructuredErrorLogger
  require 'json'
  require 'logger'

  def initialize(logger = Logger.new(STDOUT))
    @logger = logger
    @logger.formatter = method(:json_formatter)
  end

  def log_exception(exception, level: :error, context: {})
    log_data = {
      timestamp: Time.now.utc.iso8601,
      level: level.to_s.upcase,
      message: exception.message,
      exception_class: exception.class.name,
      backtrace: extract_structured_backtrace(exception),
      context: context,
      process_info: {
        pid: Process.pid,
        thread_id: Thread.current.object_id
      }
    }

    @logger.public_send(level, log_data)
  end

  private

  def extract_structured_backtrace(exception)
    return [] unless exception.backtrace_locations

    exception.backtrace_locations.first(20).map do |location|
      {
        file: location.path,
        line: location.lineno,
        method: location.label,
        absolute_path: location.absolute_path
      }
    end
  end

  def json_formatter(severity, datetime, progname, msg)
    case msg
    when Hash
      "#{msg.to_json}\n"
    else
      {
        timestamp: datetime.utc.iso8601,
        level: severity,
        message: msg.to_s
      }.to_json + "\n"
    end
  end
end

Common Pitfalls

Backtrace modification and filtering can inadvertently remove crucial debugging information. Developers often filter too aggressively, eliminating frames that provide important context about the error's root cause. Proper filtering requires understanding application boundaries and preserving sufficient context for debugging.

String-based backtrace parsing introduces fragility when Ruby's backtrace format changes between versions or when dealing with edge cases like eval statements, blocks, and metaprogramming. Using backtrace_locations provides more robust access to structured frame information.

# Problematic: Fragile string parsing
def extract_method_name_bad(backtrace_line)
  # Breaks with different backtrace formats
  backtrace_line.split(':in `')[1]&.chomp("'")
end

# Better: Use structured backtrace locations
def extract_method_name_good(backtrace_location)
  backtrace_location.label
end

# Example showing the difference
begin
  eval("def dynamic_method; raise 'error'; end; dynamic_method", binding, "eval_file.rb", 10)
rescue => e
  string_line = e.backtrace.first
  location_obj = e.backtrace_locations.first
  
  puts "String parsing result: #{extract_method_name_bad(string_line)}"
  puts "Location object result: #{extract_method_name_good(location_obj)}"
end

Memory consumption becomes problematic when storing full backtraces for high-frequency errors or in applications with deep call stacks. Backtrace arrays can contain hundreds of strings, each with file paths and method names, leading to significant memory overhead in error-heavy applications.

class BacktraceMemoryManager
  MAX_FRAMES = 50
  MAX_FRAME_LENGTH = 200

  def self.truncate_backtrace(backtrace, preserve_top: 10, preserve_bottom: 5)
    return backtrace unless backtrace && backtrace.length > MAX_FRAMES

    top_frames = backtrace.first(preserve_top)
    bottom_frames = backtrace.last(preserve_bottom)
    
    truncated = top_frames + ["... #{backtrace.length - preserve_top - preserve_bottom} frames omitted ..."] + bottom_frames
    
    # Limit individual frame length
    truncated.map do |frame|
      frame.length > MAX_FRAME_LENGTH ? "#{frame[0, MAX_FRAME_LENGTH]}..." : frame
    end
  end

  def self.estimate_backtrace_memory(backtrace)
    return 0 unless backtrace
    backtrace.sum { |frame| frame.bytesize + 40 } # 40 bytes overhead per string object
  end

  # Example usage
  def self.safe_backtrace_capture(exception)
    original = exception.backtrace
    return nil unless original

    memory_estimate = estimate_backtrace_memory(original)
    
    if memory_estimate > 10_000 # 10KB limit
      truncated = truncate_backtrace(original)
      puts "Backtrace truncated: #{original.length} -> #{truncated.length} frames"
      truncated
    else
      original
    end
  end
end

Thread safety issues arise when sharing exception objects between threads or when accessing backtrace information from multiple threads concurrently. Exception objects are not inherently thread-safe, and backtrace modification can cause race conditions.

class ThreadSafeErrorReporter
  def initialize
    @error_queue = Queue.new
    @reporter_thread = start_reporter_thread
  end

  def report_error(exception, context = {})
    # Create immutable error snapshot
    error_snapshot = {
      class: exception.class.name,
      message: exception.message.dup.freeze,
      backtrace: exception.backtrace&.map(&:dup)&.map(&:freeze)&.freeze,
      context: context.dup.freeze,
      thread_id: Thread.current.object_id,
      captured_at: Time.now.dup.freeze
    }.freeze

    @error_queue << error_snapshot
  end

  private

  def start_reporter_thread
    Thread.new do
      loop do
        begin
          error_data = @error_queue.pop
          process_error_data(error_data)
        rescue => e
          # Avoid infinite error reporting loop
          STDERR.puts "Error reporter failed: #{e.message}"
        end
      end
    end
  end

  def process_error_data(error_data)
    # Safe to access frozen data from different thread
    puts "Processing error from thread #{error_data[:thread_id]}"
    puts "Error: #{error_data[:class]}: #{error_data[:message]}"
    error_data[:backtrace]&.first(5)&.each { |frame| puts "  #{frame}" }
  end
end

Custom exception classes often fail to properly handle backtrace inheritance and cause chaining. Developers sometimes override backtrace-related methods incorrectly, breaking the standard exception interface and causing issues with error reporting tools.

# Problematic custom exception
class BadCustomException < StandardError
  def initialize(message, custom_data)
    super(message)
    @custom_data = custom_data
    # This breaks backtrace functionality
    @backtrace = caller  # Wrong: captures backtrace too early
  end

  def backtrace
    @backtrace  # Wrong: returns stale backtrace
  end
end

# Correct custom exception implementation
class GoodCustomException < StandardError
  attr_reader :custom_data

  def initialize(message, custom_data = {})
    super(message)
    @custom_data = custom_data
    # Let Ruby handle backtrace automatically
  end

  def to_h
    {
      class: self.class.name,
      message: message,
      custom_data: @custom_data,
      backtrace: backtrace
    }
  end
end

# Example showing proper cause chaining
class ChainedExceptionExample
  def self.demonstrate_proper_chaining
    begin
      begin
        raise ArgumentError, "Original error"
      rescue ArgumentError => original
        # Proper cause chaining
        raise GoodCustomException.new("Wrapped error", {context: "processing"}), cause: original
      end
    rescue GoodCustomException => e
      puts "Main error: #{e.message}"
      puts "Caused by: #{e.cause.class}: #{e.cause.message}"
      puts "Chain length: #{count_cause_chain(e)}"
    end
  end

  def self.count_cause_chain(exception)
    count = 0
    current = exception
    while current
      count += 1
      current = current.cause
    end
    count
  end
end

Reference

Exception Backtrace Methods

Method Parameters Returns Description
Exception#backtrace None Array<String> or nil Returns array of backtrace strings
Exception#backtrace_locations None Array<Thread::Backtrace::Location> or nil Returns structured backtrace objects
Exception#set_backtrace(bt) bt (Array or String) Array<String> Sets custom backtrace
Exception#cause None Exception or nil Returns the exception that caused this exception

Thread::Backtrace::Location Methods

Method Parameters Returns Description
#absolute_path None String or nil Absolute file path
#base_label None String or nil Base method name without decoration
#label None String or nil Method or block label
#lineno None Integer Line number
#path None String File path as given to Ruby
#to_s None String String representation of location

Kernel Backtrace Methods

Method Parameters Returns Description
caller(start=1, length=nil) start (Integer), length (Integer) Array<String> or nil Returns backtrace of current stack
caller_locations(start=1, length=nil) start (Integer), length (Integer) Array<Thread::Backtrace::Location> or nil Returns location objects for current stack

Backtrace String Format Patterns

Context Format Pattern Example
Method call file:line:in 'method' app.rb:15:in 'process_data'
Block file:line:in 'block in method' app.rb:20:in 'block in each'
Class definition file:line:in '<class:ClassName>' model.rb:5:in '<class:User>'
Module definition file:line:in '<module:ModuleName>' lib.rb:10:in '<module:Utils>'
Eval context (eval):line:in 'method' (eval):1:in 'dynamic_method'
Main program file:line:in '<main>' script.rb:1:in '<main>'

Common Backtrace Filtering Patterns

Pattern Type Regular Expression Description
Gem files %r{/gems/[^/]+/} Matches installed gem paths
Ruby stdlib %r{/lib/ruby/\d+\.\d+\.\d+/} Matches Ruby standard library
Internal Ruby %r{<internal:} Matches Ruby internal methods
Framework paths %r{/(rails|rack)/} Matches common framework paths
System paths %r{^/usr/} Matches system installation paths

Exception Cause Chain Methods

Operation Code Pattern Description
Chain exceptions raise NewError, cause: original Links new exception to original
Walk cause chain current = exception; while current; current = current.cause; end Iterates through exception chain
Count chain depth count = 0; current = exception; while current; count += 1; current = current.cause; end Counts exceptions in chain

Backtrace Memory Considerations

Scenario Memory Impact Mitigation Strategy
Deep recursion High - hundreds of frames Truncate to preserve top/bottom frames
High error frequency High - many stored backtraces Implement backtrace deduplication
Long file paths Medium - path strings consume memory Truncate or relativize paths
Production logging Medium - persistent storage Compress or sample backtraces

Performance Characteristics

Operation Time Complexity Notes
#backtrace O(n) Linear with stack depth
#backtrace_locations O(n) Linear with stack depth, creates objects
#set_backtrace O(n) Linear with backtrace array size
String parsing O(m) Linear with string length per frame
Location object access O(1) Constant time for attribute access