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 |