CrackedRuby logo

CrackedRuby

set_backtrace Enhancement

Overview

The set_backtrace method in Ruby provides direct control over exception backtrace information by allowing developers to replace the default backtrace with custom stack trace data. Ruby implements this functionality through the Exception class, making it available on all exception objects including StandardError, RuntimeError, and custom exception classes.

Ruby stores backtrace information as an array of strings, where each string represents one frame in the call stack. The set_backtrace method accepts either an array of strings or a single string, converting single strings into single-element arrays internally. This mechanism supports both programmatic backtrace generation and backtrace manipulation for debugging purposes.

The method operates by directly replacing the exception's internal backtrace storage, bypassing Ruby's automatic backtrace generation that normally occurs when exceptions are raised. This replacement happens immediately upon method invocation and affects all subsequent backtrace access through methods like backtrace, backtrace_locations, and full_message.

# Basic backtrace replacement
begin
  raise "Original error"
rescue => e
  e.set_backtrace(["custom_file.rb:10:in `custom_method'"])
  puts e.backtrace
  # => ["custom_file.rb:10:in `custom_method'"]
end

Ruby applications commonly use set_backtrace in error reporting libraries, testing frameworks, and debugging tools where precise control over backtrace presentation improves error diagnosis. The method also supports backtrace filtering scenarios where applications need to hide internal implementation details or highlight specific code paths.

# Creating exception with custom backtrace
error = RuntimeError.new("Configuration error")
error.set_backtrace([
  "config/application.rb:45:in `load_settings'",
  "config/boot.rb:12:in `initialize'",
  "main.rb:5:in `<main>'"
])
raise error

Basic Usage

The set_backtrace method accepts backtrace data in multiple formats and immediately replaces any existing backtrace information. The most common usage pattern involves capturing an exception, modifying its backtrace, and then either re-raising the exception or logging it with the modified trace information.

String array format represents the standard approach, where each array element contains location information following Ruby's backtrace conventions. The format typically includes filename, line number, and method name using the pattern "filename:line:in 'method_name'", though Ruby accepts any string format.

# Standard array format
exception = StandardError.new("Database connection failed")
exception.set_backtrace([
  "lib/database.rb:156:in `connect'",
  "app/models/user.rb:23:in `find'",
  "app/controllers/users_controller.rb:15:in `show'"
])

puts exception.backtrace
# => ["lib/database.rb:156:in `connect'",
#     "app/models/user.rb:23:in `find'",
#     "app/controllers/users_controller.rb:15:in `show'"]

Single string input automatically converts to a single-element array, making it convenient for simple backtrace scenarios where only one location needs specification. This approach works well for custom exceptions that represent specific error conditions rather than propagated failures.

# Single string conversion
error = ArgumentError.new("Invalid email format")
error.set_backtrace("validators/email_validator.rb:42:in `validate'")

puts error.backtrace.class
# => Array
puts error.backtrace.length
# => 1
puts error.backtrace.first
# => "validators/email_validator.rb:42:in `validate'"

Backtrace manipulation commonly occurs in rescue blocks where applications need to preserve original error context while adding custom debugging information. This pattern maintains the original exception type while providing more meaningful location data for error diagnosis.

# Rescue and modify pattern
def process_user_data(data)
  user_service.validate(data)
rescue ValidationError => e
  # Preserve error but add context
  e.set_backtrace([
    "#{__FILE__}:#{__LINE__}:in `#{__method__}'",
    "processing step: data validation"
  ] + (e.backtrace || []))
  raise e
end

Empty array input removes all backtrace information, creating exceptions with no stack trace data. This technique proves useful for generic error messages or when backtrace information might confuse end users rather than help with debugging.

# Clearing backtrace completely
user_error = RuntimeError.new("Please check your input")
user_error.set_backtrace([])

puts user_error.backtrace
# => []
puts user_error.full_message
# => "Please check your input (RuntimeError)"

Advanced Usage

Complex error handling systems often require sophisticated backtrace manipulation that goes beyond simple string replacement. Ruby's set_backtrace method supports advanced patterns including backtrace synthesis, stack trace merging, and dynamic backtrace generation based on runtime conditions.

Backtrace synthesis creates entirely artificial stack traces that represent logical program flow rather than actual method calls. This technique proves valuable in domain-specific languages, configuration systems, and template engines where the actual Ruby call stack provides less meaningful debugging information than a synthesized trace showing logical execution steps.

class TemplateProcessor
  def self.process(template_name, context)
    steps = []

    begin
      steps << "Loading template: #{template_name}"
      template = load_template(template_name)

      steps << "Processing variables"
      processed = substitute_variables(template, context)

      steps << "Rendering output"
      render_output(processed)

    rescue => original_error
      # Create logical backtrace from processing steps
      synthetic_trace = steps.each_with_index.map do |step, index|
        "template_processor:#{index + 1}:in `#{step}'"
      end

      # Combine with relevant parts of actual backtrace
      actual_trace = original_error.backtrace.select do |frame|
        frame.include?('template') || frame.include?('render')
      end

      error = ProcessingError.new("Template processing failed: #{original_error.message}")
      error.set_backtrace(synthetic_trace + actual_trace)
      raise error
    end
  end
end

Stack trace merging combines backtraces from multiple exception sources, creating comprehensive error reports that show the complete failure chain. This pattern becomes essential in distributed systems, async processing, and callback-heavy architectures where errors propagate through multiple execution contexts.

class AsyncTaskRunner
  def execute_with_callbacks(task, callbacks = {})
    results = []
    errors = []

    begin
      result = task.call
      results << result

      # Execute success callbacks
      callbacks[:on_success]&.each do |callback|
        begin
          callback.call(result)
        rescue => callback_error
          errors << {
            source: :callback,
            error: callback_error,
            context: "success callback #{callbacks[:on_success].index(callback)}"
          }
        end
      end

    rescue => task_error
      errors << {
        source: :task,
        error: task_error,
        context: "main task execution"
      }

      # Execute error callbacks
      callbacks[:on_error]&.each do |callback|
        begin
          callback.call(task_error)
        rescue => callback_error
          errors << {
            source: :error_callback,
            error: callback_error,
            context: "error callback #{callbacks[:on_error].index(callback)}"
          }
        end
      end
    end

    # Merge all error backtraces if any occurred
    unless errors.empty?
      merged_backtrace = errors.flat_map do |error_info|
        ["--- #{error_info[:context]} ---"] +
        (error_info[:error].backtrace || ["<no backtrace available>"])
      end

      summary_error = RuntimeError.new("Multiple failures in async execution")
      summary_error.set_backtrace(merged_backtrace)
      raise summary_error
    end

    results
  end
end

Dynamic backtrace generation creates context-sensitive stack traces that adapt based on runtime conditions, configuration settings, or debugging levels. This approach enables fine-grained control over error reporting detail while maintaining performance in production environments.

module DebugTracker
  class << self
    attr_accessor :debug_level, :trace_filters

    def enhanced_backtrace(original_exception, context = {})
      base_trace = original_exception.backtrace || []

      case debug_level
      when :minimal
        # Show only application code
        filtered_trace = base_trace.select { |frame| frame.include?('/app/') }

      when :moderate
        # Add context information
        context_trace = context.map do |key, value|
          "context:#{key} => #{value.inspect}"
        end
        filtered_trace = context_trace + base_trace

      when :verbose
        # Include full trace plus environment details
        env_trace = [
          "ruby_version: #{RUBY_VERSION}",
          "load_path_size: #{$LOAD_PATH.size}",
          "thread_id: #{Thread.current.object_id}",
          "memory_usage: #{GC.stat[:heap_live_slots]} objects"
        ]
        filtered_trace = env_trace + context.map { |k,v| "#{k}: #{v}" } + base_trace

      else
        filtered_trace = base_trace
      end

      # Apply custom filters
      if trace_filters
        trace_filters.each do |filter|
          filtered_trace = filter.call(filtered_trace)
        end
      end

      filtered_trace
    end

    def wrap_with_enhanced_backtrace(exception, **context)
      enhanced_trace = enhanced_backtrace(exception, context)
      exception.set_backtrace(enhanced_trace)
      exception
    end
  end
end

# Usage example
DebugTracker.debug_level = :moderate
DebugTracker.trace_filters = [
  ->(trace) { trace.reject { |frame| frame.include?('vendor/') } }
]

begin
  risky_operation
rescue => e
  enhanced_error = DebugTracker.wrap_with_enhanced_backtrace(
    e,
    user_id: current_user.id,
    request_id: request.uuid,
    operation: 'user_data_sync'
  )
  raise enhanced_error
end

Error Handling & Debugging

Ruby's set_backtrace method provides critical debugging capabilities by enabling precise control over error context presentation. Effective error handling strategies combine backtrace manipulation with structured logging, error categorization, and context preservation to create comprehensive debugging workflows.

Backtrace filtering removes noise from stack traces while preserving essential debugging information. Production applications often generate deep backtraces that include framework internals, gem code, and Ruby standard library calls that obscure the actual error location. Strategic filtering highlights application-specific code paths and relevant external dependencies.

module BacktraceFilter
  NOISE_PATTERNS = [
    %r{gems/.*/(lib|bin)/},                    # Gem internals
    %r{ruby/\d+\.\d+\.\d+/lib/},              # Ruby standard library
    %r{bundler/gems/},                         # Bundler managed gems
    %r{/usr/lib/ruby/},                        # System Ruby
    %r{\(eval\)},                              # Dynamic evaluation
    %r{block \(\d+ levels\) in}               # Deep block nesting
  ].freeze

  APPLICATION_PATTERNS = [
    %r{/app/},                                 # Main application
    %r{/lib/.*\.rb},                          # Local libraries
    %r{/config/},                             # Configuration files
    %r{spec/.*\.rb}                           # Test files
  ].freeze

  def self.filter_backtrace(exception, mode: :balanced)
    original_trace = exception.backtrace || []

    filtered_trace = case mode
    when :application_only
      original_trace.select { |frame| APPLICATION_PATTERNS.any? { |pattern| frame.match?(pattern) } }

    when :noise_removed
      original_trace.reject { |frame| NOISE_PATTERNS.any? { |pattern| frame.match?(pattern) } }

    when :balanced
      # Keep application frames and important external frames
      original_trace.select do |frame|
        APPLICATION_PATTERNS.any? { |pattern| frame.match?(pattern) } ||
        (!NOISE_PATTERNS.any? { |pattern| frame.match?(pattern) } && frame.include?('.rb'))
      end

    else
      original_trace
    end

    # Always preserve at least one frame
    filtered_trace = original_trace.first(3) if filtered_trace.empty?

    exception.set_backtrace(filtered_trace)
    exception
  end
end

# Usage in error handling
def process_request(params)
  validator.validate(params)
  data_processor.process(params)
  response_builder.build(processed_data)
rescue ValidationError, ProcessingError => e
  # Filter but preserve validation/processing context
  BacktraceFilter.filter_backtrace(e, mode: :balanced)
  logger.error("Request processing failed", {
    error: e.class.name,
    message: e.message,
    backtrace: e.backtrace.first(5)
  })
  raise e
end

Context preservation maintains debugging information while transforming backtraces for different audiences. Development environments benefit from verbose traces, while production systems require filtered traces that protect sensitive information and reduce log volume without sacrificing debugging capability.

class ContextualErrorHandler
  def self.handle_error(exception, context = {})
    # Preserve original backtrace for internal debugging
    original_backtrace = exception.backtrace&.dup || []

    # Create user-friendly backtrace
    user_backtrace = create_user_backtrace(original_backtrace, context)

    # Create developer backtrace with full context
    developer_backtrace = create_developer_backtrace(original_backtrace, context)

    # Log full details for developers
    Rails.logger.error({
      error_class: exception.class.name,
      error_message: exception.message,
      context: context,
      full_backtrace: developer_backtrace,
      user_agent: context[:user_agent],
      request_id: context[:request_id]
    }.to_json)

    # Set user-friendly backtrace for error reporting
    exception.set_backtrace(user_backtrace)

    exception
  end

  private

  def self.create_user_backtrace(original_backtrace, context)
    # Extract meaningful application frames
    app_frames = original_backtrace.select do |frame|
      frame.include?('/app/') && !frame.include?('/vendor/')
    end

    # Add context breadcrumbs
    context_frames = context.slice(:controller, :action, :params).map do |key, value|
      "#{key}: #{value}"
    end

    (context_frames + app_frames).first(10)
  end

  def self.create_developer_backtrace(original_backtrace, context)
    debug_info = [
      "=== Request Context ===",
      "Timestamp: #{Time.current.iso8601}",
      "Thread: #{Thread.current.object_id}",
      "Memory: #{GC.stat[:heap_live_slots]} objects"
    ]

    context_info = context.map { |k, v| "#{k}: #{v.inspect}" }

    debug_info + context_info + ["=== Stack Trace ==="] + original_backtrace
  end
end

Error propagation scenarios require careful backtrace management to maintain debugging information across multiple layers while avoiding information loss. Complex applications often catch, transform, and re-raise exceptions multiple times, requiring strategies to preserve the complete error history.

class ErrorChain
  attr_reader :errors, :primary_error

  def initialize(primary_error)
    @primary_error = primary_error
    @errors = [primary_error]
  end

  def add_context_error(error, context)
    # Preserve original backtrace in context
    enhanced_backtrace = [
      "=== Context: #{context} ===",
      *error.backtrace
    ]

    if @errors.size > 1
      enhanced_backtrace += ["=== Previous Error Chain ==="] +
                           @errors.last.backtrace.first(5)
    end

    error.set_backtrace(enhanced_backtrace)
    @errors << error
    self
  end

  def raise_with_full_context
    final_error = @errors.last

    # Create comprehensive backtrace showing full error chain
    chain_trace = @errors.each_with_index.flat_map do |error, index|
      separator = index == 0 ? "=== Primary Error ===" : "=== Chained Error #{index} ==="
      [separator, "#{error.class}: #{error.message}"] + (error.backtrace || [])
    end

    final_error.set_backtrace(chain_trace)
    raise final_error
  end
end

# Usage in complex error scenarios
def multi_step_operation
  error_chain = nil

  begin
    step_one
  rescue => e
    error_chain = ErrorChain.new(e)
  end

  if error_chain
    begin
      recovery_step_one
    rescue => e
      error_chain.add_context_error(e, "recovery attempt")
    end
  end

  begin
    step_two
  rescue => e
    if error_chain
      error_chain.add_context_error(e, "step two failure")
      error_chain.raise_with_full_context
    else
      raise e
    end
  end
end

Testing Strategies

Testing backtrace manipulation requires specialized approaches that validate both backtrace content and format while maintaining test reliability across different Ruby versions and environments. Effective testing strategies combine backtrace content verification with exception behavior validation and mock backtrace scenarios.

Backtrace content testing validates that set_backtrace correctly applies custom stack trace information and preserves the expected format. These tests must account for Ruby's internal backtrace representation while focusing on application-specific backtrace logic rather than Ruby's internal exception handling.

RSpec.describe "Custom backtrace handling" do
  describe "backtrace modification" do
    it "replaces backtrace with custom frames" do
      original_error = StandardError.new("Test error")
      custom_frames = [
        "custom_file.rb:10:in `custom_method'",
        "other_file.rb:25:in `other_method'"
      ]

      original_error.set_backtrace(custom_frames)

      expect(original_error.backtrace).to eq(custom_frames)
      expect(original_error.backtrace.size).to eq(2)
    end

    it "converts single string to array" do
      error = RuntimeError.new("Single frame error")
      single_frame = "single_file.rb:15:in `single_method'"

      error.set_backtrace(single_frame)

      expect(error.backtrace).to be_a(Array)
      expect(error.backtrace).to eq([single_frame])
    end

    it "handles empty backtrace" do
      error = ArgumentError.new("No trace error")
      error.set_backtrace([])

      expect(error.backtrace).to eq([])
      expect(error.backtrace_locations).to be_nil
    end
  end

  describe "backtrace filtering" do
    let(:noisy_backtrace) do
      [
        "/app/models/user.rb:45:in `find_user'",
        "/gems/activerecord/lib/active_record/base.rb:123:in `find'",
        "/usr/lib/ruby/2.7.0/psych.rb:456:in `parse'",
        "/app/controllers/users_controller.rb:12:in `show'",
        "(eval):1:in `block'"
      ]
    end

    it "filters noise while preserving application frames" do
      error = StandardError.new("Database error")
      error.set_backtrace(noisy_backtrace)

      filtered_error = BacktraceFilter.filter_backtrace(error, mode: :application_only)

      expect(filtered_error.backtrace).to include(
        "/app/models/user.rb:45:in `find_user'",
        "/app/controllers/users_controller.rb:12:in `show'"
      )
      expect(filtered_error.backtrace).not_to include(
        match(%r{gems/activerecord}),
        match(%r{usr/lib/ruby}),
        match(%r{\(eval\)})
      )
    end
  end
end

Mock backtrace scenarios test error handling logic without depending on actual exception raising, enabling precise control over backtrace conditions and edge cases. This approach isolates backtrace manipulation logic from the complexities of real exception generation.

RSpec.describe ErrorChain do
  let(:mock_error_with_backtrace) do
    error = double("MockError")
    allow(error).to receive(:backtrace).and_return([
      "mock_file.rb:1:in `mock_method'",
      "test_file.rb:2:in `test_method'"
    ])
    allow(error).to receive(:set_backtrace) do |backtrace|
      allow(error).to receive(:backtrace).and_return(backtrace)
    end
    allow(error).to receive(:class).and_return(RuntimeError)
    allow(error).to receive(:message).and_return("Mock error message")
    error
  end

  describe "#add_context_error" do
    it "preserves error chain in backtrace" do
      primary_error = mock_error_with_backtrace
      chain = ErrorChain.new(primary_error)

      context_error = mock_error_with_backtrace
      chain.add_context_error(context_error, "validation context")

      expect(context_error).to have_received(:set_backtrace) do |backtrace|
        expect(backtrace).to include("=== Context: validation context ===")
        expect(backtrace).to include("mock_file.rb:1:in `mock_method'")
      end
    end

    it "includes previous error information in chain" do
      primary_error = mock_error_with_backtrace
      chain = ErrorChain.new(primary_error)

      second_error = mock_error_with_backtrace
      chain.add_context_error(second_error, "second context")

      third_error = mock_error_with_backtrace
      chain.add_context_error(third_error, "third context")

      expect(third_error).to have_received(:set_backtrace) do |backtrace|
        expect(backtrace).to include("=== Context: third context ===")
        expect(backtrace).to include("=== Previous Error Chain ===")
      end
    end
  end
end

Integration testing validates backtrace behavior in realistic scenarios that combine exception raising, catching, and re-raising with backtrace modification. These tests ensure that backtrace manipulation works correctly within actual Ruby exception handling flows.

RSpec.describe "Backtrace integration scenarios" do
  let(:test_service) do
    Class.new do
      def self.failing_operation
        nested_method_call
      end

      def self.nested_method_call
        raise StandardError, "Deep failure"
      end

      def self.wrapped_operation
        failing_operation
      rescue => e
        wrapped_error = RuntimeError.new("Wrapped: #{e.message}")
        wrapped_error.set_backtrace([
          "wrapper.rb:10:in `wrapped_operation'",
          *e.backtrace.first(3)
        ])
        raise wrapped_error
      end
    end
  end

  it "preserves custom backtrace through re-raising" do
    expect {
      test_service.wrapped_operation
    }.to raise_error(RuntimeError, /Wrapped:/) do |error|
      expect(error.backtrace.first).to eq("wrapper.rb:10:in `wrapped_operation'")
      expect(error.backtrace).to include(match(/nested_method_call/))
      expect(error.backtrace.size).to be <= 4  # Wrapper + first 3 original frames
    end
  end

  it "maintains backtrace through multiple rescue levels" do
    outer_service = Class.new do
      def self.process
        test_service.wrapped_operation
      rescue RuntimeError => e
        # Add another layer of context
        final_error = ProcessingError.new("Processing failed")
        final_error.set_backtrace([
          "outer_service.rb:5:in `process'",
          "=== Caused by ===",
          *e.backtrace
        ])
        raise final_error
      end
    end

    expect {
      outer_service.process
    }.to raise_error(ProcessingError) do |error|
      expect(error.backtrace).to include(
        "outer_service.rb:5:in `process'",
        "=== Caused by ===",
        "wrapper.rb:10:in `wrapped_operation'"
      )
    end
  end
end

# Custom error class for testing
class ProcessingError < StandardError; end

Common Pitfalls

Ruby's set_backtrace method presents several subtle behaviors that can lead to debugging difficulties and unexpected error handling issues. Understanding these pitfalls prevents common mistakes and ensures reliable backtrace manipulation in production applications.

Backtrace format inconsistencies create debugging confusion when custom backtraces don't follow Ruby's expected format conventions. Ruby's default backtrace format includes specific patterns for file locations, line numbers, and method names that debugging tools and error reporting systems expect. Deviating from these patterns can break integration with development tools and log parsing systems.

# PROBLEMATIC: Inconsistent backtrace formats
error = StandardError.new("Format issue")

# This breaks debugging tool expectations
error.set_backtrace([
  "somewhere in the code",           # No file/line information
  "file.rb - line 25",              # Wrong separator format
  "/app/models/user.rb(30) method",  # Parentheses instead of colon
  "line 45 in some_method"          # Reversed order
])

# Ruby expects this format pattern:
expected_backtrace = [
  "/app/models/user.rb:30:in `some_method'",     # Standard format
  "/app/controllers/base.rb:45:in `handle_error'", # Consistent pattern
  "/usr/lib/ruby/3.0/lib/ruby/pathname.rb:123:in `+'", # Full paths work
  "(irb):1:in `<main>'"                         # Special cases like REPL
]

# CORRECT: Maintain consistent formatting
error.set_backtrace(expected_backtrace)

# Demonstrate the difference in debugging output
puts "=== Problematic format ==="
error.set_backtrace(["somewhere in the code"])
puts error.full_message

puts "\n=== Standard format ==="
error.set_backtrace(["/app/models/user.rb:30:in `some_method'"])
puts error.full_message

Memory accumulation occurs when backtrace manipulation inadvertently creates memory leaks through retained references or excessive backtrace data. Large backtraces consume significant memory, and applications that frequently create exceptions with extensive custom backtraces can experience memory pressure, especially in high-throughput scenarios.

# PROBLEMATIC: Memory accumulation through large backtraces
class VerboseErrorHandler
  @@error_history = []  # Class variable retains all errors

  def self.create_detailed_error(original_error, context)
    # Accumulating massive amounts of debug data
    detailed_backtrace = [
      "=== Full System State ===",
      "Ruby version: #{RUBY_VERSION}",
      "Platform: #{RUBY_PLATFORM}",
      "Load path: #{$LOAD_PATH.join('; ')}",  # Potentially huge
      "All constants: #{Object.constants.join('; ')}", # Very large
      "Environment: #{ENV.to_h.inspect}",     # Contains sensitive data
      "=== Object Space ===",
      *ObjectSpace.each_object.map(&:to_s),   # Enormous and slow
      "=== Original Error ===",
      *original_error.backtrace
    ]

    new_error = StandardError.new(original_error.message)
    new_error.set_backtrace(detailed_backtrace)

    # Retaining references causes memory leaks
    @@error_history << new_error

    new_error
  end
end

# CORRECT: Memory-conscious error handling
class EfficientErrorHandler
  MAX_BACKTRACE_SIZE = 50
  MAX_CONTEXT_ITEMS = 10

  def self.create_contextual_error(original_error, context = {})
    # Limit context to essential information
    essential_context = context.slice(*%i[user_id request_id operation])
                               .first(MAX_CONTEXT_ITEMS)

    context_frames = essential_context.map { |k, v| "#{k}: #{v.to_s[0, 100]}" }
    original_frames = (original_error.backtrace || []).first(MAX_BACKTRACE_SIZE - context_frames.size)

    new_error = original_error.class.new(original_error.message)
    new_error.set_backtrace(context_frames + original_frames)

    new_error
  end
end

# Memory usage comparison
require 'objspace'

# Measure memory impact
before_size = ObjectSpace.memsize_of_all
problematic_error = VerboseErrorHandler.create_detailed_error(
  StandardError.new("test"),
  { huge_data: "x" * 10000 }
)
after_problematic = ObjectSpace.memsize_of_all

before_efficient = ObjectSpace.memsize_of_all
efficient_error = EfficientErrorHandler.create_contextual_error(
  StandardError.new("test"),
  { user_id: 123, request_id: "abc" }
)
after_efficient = ObjectSpace.memsize_of_all

puts "Problematic approach memory impact: #{after_problematic - before_size} bytes"
puts "Efficient approach memory impact: #{after_efficient - before_efficient} bytes"

Thread safety issues emerge when backtrace manipulation occurs in concurrent environments where multiple threads might modify the same exception objects or share backtrace data. Ruby's exception objects are not inherently thread-safe, and concurrent backtrace modification can lead to race conditions and corrupted stack trace information.

# PROBLEMATIC: Thread-unsafe backtrace sharing
class SharedErrorHandler
  @@shared_context = {}
  @@template_error = StandardError.new("Shared template error")

  def self.create_error_for_thread(thread_id, context)
    # Race condition: multiple threads modifying shared state
    @@shared_context.merge!(context)

    # Using shared error object across threads
    backtrace_template = [
      "thread_#{thread_id}: processing",
      "shared_context: #{@@shared_context.inspect}",  # Inconsistent across threads
      *@@template_error.backtrace  # Shared object mutation
    ]

    @@template_error.set_backtrace(backtrace_template)
    @@template_error  # Returning same object to multiple threads
  end
end

# CORRECT: Thread-safe error handling
class ThreadSafeErrorHandler
  def self.create_error_for_thread(thread_id, context)
    # Create fresh error object for each thread
    thread_error = StandardError.new("Thread #{thread_id} error")

    # Use thread-local context instead of shared state
    Thread.current[:error_context] ||= {}
    Thread.current[:error_context].merge!(context)

    # Build backtrace from thread-local data
    thread_backtrace = [
      "thread_#{thread_id}:#{Thread.current.object_id}:in `process'",
      "context: #{Thread.current[:error_context].inspect}",
      "caller: #{caller(1).first}"
    ]

    thread_error.set_backtrace(thread_backtrace)
    thread_error
  end
end

# Demonstrate thread safety issues
require 'thread'

threads = 10.times.map do |i|
  Thread.new do
    sleep(rand * 0.1)  # Random timing to trigger race conditions

    # Problematic approach
    shared_error = SharedErrorHandler.create_error_for_thread(i, { data: "thread_#{i}_data" })
    puts "Shared error thread #{i}: #{shared_error.backtrace.first}"

    # Thread-safe approach
    safe_error = ThreadSafeErrorHandler.create_error_for_thread(i, { data: "thread_#{i}_data" })
    puts "Safe error thread #{i}: #{safe_error.backtrace.first}"
  end
end

threads.each(&:join)

Exception type mismatches occur when backtrace manipulation fails to consider exception inheritance and type-specific behavior. Different exception classes may have specialized backtrace handling or additional metadata that gets lost during backtrace modification, leading to incomplete error information.

# PROBLEMATIC: Ignoring exception-specific behavior
begin
  raise ArgumentError, "Invalid argument: expected Integer, got String"
rescue => e
  # Losing ArgumentError-specific behavior by creating generic error
  generic_error = StandardError.new(e.message)
  generic_error.set_backtrace(["generic_location.rb:1:in `generic_method'"])

  # ArgumentError has specific semantics that are lost
  raise generic_error  # Now just a StandardError
end

# CORRECT: Preserve exception type and behavior
class TypePreservingErrorHandler
  def self.enhance_error(original_error, additional_context)
    # Preserve original exception type
    enhanced_error = original_error.class.new(original_error.message)

    # Copy any exception-specific attributes
    case original_error
    when ArgumentError
      # ArgumentError might have additional context
      enhanced_error.define_singleton_method(:argument_details) do
        original_error.respond_to?(:argument_details) ? original_error.argument_details : nil
      end

    when NoMethodError
      # Preserve method name and receiver information if available
      if original_error.respond_to?(:name)
        enhanced_error.define_singleton_method(:name) { original_error.name }
      end
      if original_error.respond_to?(:receiver)
        enhanced_error.define_singleton_method(:receiver) { original_error.receiver }
      end

    when SystemCallError
      # Preserve errno information
      enhanced_error.define_singleton_method(:errno) { original_error.errno }
    end

    # Build enhanced backtrace while preserving context
    context_info = additional_context.map { |k, v| "#{k}: #{v}" }
    enhanced_backtrace = context_info + (original_error.backtrace || [])

    enhanced_error.set_backtrace(enhanced_backtrace)
    enhanced_error
  end
end

# Test exception type preservation
begin
  "string".to_i(invalid: "argument")  # Raises ArgumentError
rescue ArgumentError => e
  enhanced = TypePreservingErrorHandler.enhance_error(e, { context: "number parsing" })
  puts "Enhanced error class: #{enhanced.class}"  # Still ArgumentError
  puts "Enhanced error message: #{enhanced.message}"
  puts "Enhanced backtrace includes context: #{enhanced.backtrace.first.include?('context:')}"
end

Reference

Core Methods

Method Parameters Returns Description
#set_backtrace(backtrace) backtrace (Array or String) Array<String> Sets custom backtrace, replacing any existing backtrace data
#backtrace None Array<String> or nil Returns current backtrace array, nil if no backtrace set
#backtrace_locations None Array<Thread::Backtrace::Location> or nil Returns backtrace location objects, nil for custom backtraces
#full_message(**opts) opts (Hash) String Returns formatted error message with backtrace

Backtrace Format Specifications

Format Component Pattern Example
Standard format "filename:line:in 'method'" "app/models/user.rb:45:in 'find_user'"
Class method "filename:line:in 'ClassName.method'" "lib/service.rb:12:in 'ApiClient.fetch'"
Block execution "filename:line:in 'block in method'" "app/worker.rb:23:in 'block in process'"
Nested blocks "filename:line:in 'block (N levels) in method'" "spec/test.rb:15:in 'block (2 levels) in describe'"
Eval context "(eval):line:in 'method'" "(eval):1:in '<main>'"
IRB/Console "(irb):line:in 'context'" "(irb):5:in '<top (required)>'"

Exception Classes and Backtrace Behavior

Exception Class Backtrace Characteristics Special Considerations
Exception Base class, supports all backtrace operations Root of exception hierarchy
StandardError Standard backtrace handling Default rescue target
ArgumentError Includes argument validation context May have additional argument details
NoMethodError Shows method name and receiver Preserves method/receiver information
SystemCallError System-level error information Includes errno and system context
SyntaxError Parse-time error location Backtrace shows syntax error location
LoadError File loading context Shows failed require/load path

Backtrace Manipulation Patterns

Pattern Use Case Implementation Strategy
Context Addition Add debugging context to existing errors Prepend context frames to original backtrace
Trace Filtering Remove noise from production backtraces Filter using regex patterns for framework code
Synthetic Traces Create logical execution traces Generate custom frames representing business logic
Chain Merging Combine multiple error sources Merge backtraces with separators and context
Thread Context Add thread-specific information Include thread ID and thread-local data
Performance Data Include timing and memory info Add performance metrics to error context

Memory and Performance Considerations

Aspect Recommendation Rationale
Backtrace Length Limit to 50-100 frames maximum Prevents excessive memory usage
Context Data Restrict context to essential fields Reduces memory footprint per exception
String Size Cap individual frame strings at 200 chars Prevents unbounded string growth
Frequency Avoid backtrace manipulation in hot paths Reduces performance impact
Retention Don't retain exceptions long-term Prevents memory leaks from backtrace data
Thread Safety Use thread-local storage for context Avoids concurrent modification issues

Common Error Patterns

Error Pattern Symptoms Solution
Format Inconsistency Debugger tools fail to parse traces Use standard "file:line:in 'method'" format
Memory Accumulation Growing memory usage over time Limit backtrace size and context data
Thread Corruption Inconsistent backtraces in concurrent code Create fresh error objects per thread
Type Information Loss Generic errors replace specific types Preserve original exception class
Context Overload Backtraces become unreadable Focus on essential debugging information
Circular References Memory leaks from retained error objects Avoid storing exceptions in class variables