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 |