CrackedRuby logo

CrackedRuby

Exception Tracking

Exception tracking in Ruby encompasses built-in exception handling mechanisms, custom error classes, stack trace analysis, and production monitoring strategies.

Metaprogramming TracePoint API
5.10.3

Overview

Ruby provides a comprehensive exception system built around the Exception class hierarchy and control flow statements. The core mechanism uses begin, rescue, ensure, and raise keywords to manage exceptional conditions during program execution.

The Exception class serves as the root of Ruby's exception hierarchy. StandardError inherits from Exception and represents recoverable errors that applications typically handle. System-level exceptions like SystemExit and Interrupt inherit directly from Exception and generally should not be rescued in application code.

begin
  risky_operation
rescue StandardError => e
  puts "Caught error: #{e.message}"
  puts "Backtrace: #{e.backtrace.first(3).join("\n")}"
ensure
  cleanup_resources
end

Ruby exceptions carry extensive metadata including message text, backtrace information, and the ability to attach custom data. The backtrace provides a complete call stack showing the execution path leading to the exception.

class DataProcessingError < StandardError
  attr_reader :record_id, :field_name
  
  def initialize(message, record_id: nil, field_name: nil)
    super(message)
    @record_id = record_id
    @field_name = field_name
  end
end

raise DataProcessingError.new(
  "Invalid data format", 
  record_id: 12345, 
  field_name: "email"
)

The raise method can accept an exception class, instance, or string message. When called without arguments inside a rescue block, it re-raises the current exception. The retry statement restarts execution from the beginning of the begin block, enabling recovery patterns.

Exception handling blocks can specify multiple rescue clauses targeting different exception types. Ruby evaluates rescue clauses in order, matching the first compatible type using the === operator. This allows inheritance-based exception handling where parent classes catch child exceptions.

Basic Usage

Exception tracking begins with strategic placement of rescue blocks around code that might fail. Ruby's exception handling integrates with method definitions, where the entire method body acts as an implicit begin block.

def process_file(filename)
  File.open(filename, 'r') do |file|
    file.each_line.with_index do |line, index|
      process_line(line)
    end
  end
rescue Errno::ENOENT
  puts "File not found: #{filename}"
  false
rescue Errno::EACCES
  puts "Permission denied: #{filename}"
  false
rescue => e
  puts "Unexpected error processing #{filename}: #{e.message}"
  false
else
  puts "Successfully processed #{filename}"
  true
ensure
  puts "Finished processing attempt for #{filename}"
end

The rescue clause without a specific exception type catches StandardError and its subclasses. The => operator assigns the caught exception to a variable for inspection. The else clause executes only if no exceptions occur, while ensure always executes regardless of exception status.

Custom exception classes provide structured error information and enable targeted handling strategies. Define exception classes to carry domain-specific data that aids in debugging and error reporting.

module APIClient
  class RequestError < StandardError
    attr_reader :status_code, :response_body, :request_url
    
    def initialize(message, status_code: nil, response_body: nil, request_url: nil)
      super(message)
      @status_code = status_code
      @response_body = response_body
      @request_url = request_url
    end
    
    def to_h
      {
        message: message,
        status_code: status_code,
        response_body: response_body,
        request_url: request_url,
        backtrace: backtrace
      }
    end
  end
end

def make_api_request(url)
  response = Net::HTTP.get_response(URI(url))
  
  unless response.code.start_with?('2')
    raise APIClient::RequestError.new(
      "API request failed",
      status_code: response.code,
      response_body: response.body,
      request_url: url
    )
  end
  
  response.body
rescue Net::TimeoutError => e
  raise APIClient::RequestError.new(
    "Request timeout", 
    request_url: url
  )
end

Exception instances provide access to backtrace information through the backtrace method, which returns an array of strings representing the call stack. The backtrace_locations method returns Thread::Backtrace::Location objects with detailed position information.

begin
  deeply_nested_method_call
rescue => e
  puts "Exception: #{e.class} - #{e.message}"
  puts "Location: #{e.backtrace_locations.first}"
  puts "Method: #{e.backtrace_locations.first.label}"
  puts "File: #{e.backtrace_locations.first.path}:#{e.backtrace_locations.first.lineno}"
end

The retry statement provides recovery mechanisms for transient failures. Combine retry with counters or exponential backoff to handle temporary network issues or resource contention.

def fetch_with_retry(url, max_attempts: 3, delay: 1)
  attempts = 0
  
  begin
    attempts += 1
    Net::HTTP.get(URI(url))
  rescue Net::TimeoutError, Net::HTTPServerError => e
    if attempts < max_attempts
      puts "Attempt #{attempts} failed: #{e.message}. Retrying in #{delay} seconds..."
      sleep(delay)
      delay *= 2  # Exponential backoff
      retry
    else
      puts "Failed after #{max_attempts} attempts"
      raise
    end
  end
end

Error Handling & Debugging

Exception debugging requires systematic analysis of error conditions, stack traces, and application state. Ruby provides several mechanisms for extracting diagnostic information from exceptions and runtime context.

The binding method captures the current execution context, including local variables and method scope. Combined with debugging tools, binding enables interactive exception analysis. The caller method returns the current call stack as an array of strings, useful for custom error reporting.

class DiagnosticError < StandardError
  attr_reader :context_binding, :local_variables, :call_stack
  
  def initialize(message, capture_context: true)
    super(message)
    
    if capture_context
      @context_binding = binding.of_caller(1)  # Requires binding_of_caller gem
      @local_variables = extract_local_variables(@context_binding)
      @call_stack = caller(1, 10)  # Skip current frame, get next 10
    end
  end
  
  private
  
  def extract_local_variables(binding)
    return {} unless binding
    
    binding.local_variables.each_with_object({}) do |var, hash|
      begin
        hash[var] = binding.local_variable_get(var)
      rescue NameError
        hash[var] = "<unavailable>"
      end
    end
  end
end

def risky_calculation(x, y, z)
  coefficient = 0.5
  result = x / y * coefficient
  
  if result > z
    raise DiagnosticError.new(
      "Result exceeds threshold: #{result} > #{z}"
    )
  end
  
  result
end

Exception filtering and transformation enable applications to present meaningful error messages while preserving debugging information. Create error boundaries that catch low-level exceptions and convert them to application-specific errors.

module ErrorBoundary
  def self.wrap_database_errors
    yield
  rescue ActiveRecord::RecordNotFound => e
    raise ApplicationError.new(
      "Resource not found",
      category: :not_found,
      original_exception: e
    )
  rescue ActiveRecord::RecordInvalid => e
    raise ApplicationError.new(
      "Validation failed: #{e.record.errors.full_messages.join(', ')}",
      category: :validation_error,
      original_exception: e,
      validation_errors: e.record.errors
    )
  rescue ActiveRecord::ConnectionTimeoutError => e
    raise ApplicationError.new(
      "Database connection timeout",
      category: :service_unavailable,
      original_exception: e,
      retry_after: 30
    )
  end
end

class ApplicationError < StandardError
  attr_reader :category, :original_exception, :metadata
  
  def initialize(message, category: :general, original_exception: nil, **metadata)
    super(message)
    @category = category
    @original_exception = original_exception
    @metadata = metadata
  end
  
  def full_backtrace
    traces = [backtrace]
    traces << original_exception.backtrace if original_exception
    traces.flatten.compact
  end
end

Stack trace filtering improves debugging by highlighting application code and hiding framework noise. Create custom backtrace cleaners that remove irrelevant frames and emphasize important execution paths.

class BacktraceCleaner
  FRAMEWORK_PATTERNS = [
    /\/gems\/.*\/lib/,
    /\/ruby\/.*\/lib/,
    /internal:/,
    /<internal:/
  ].freeze
  
  APP_PATTERNS = [
    /\/app\//,
    /\/lib\//,
    /\/config\//
  ].freeze
  
  def self.clean(backtrace)
    return [] unless backtrace
    
    app_frames = backtrace.select { |frame| app_frame?(frame) }
    return app_frames unless app_frames.empty?
    
    # If no app frames, return filtered framework frames
    backtrace.reject { |frame| noise_frame?(frame) }.first(10)
  end
  
  def self.app_frame?(frame)
    APP_PATTERNS.any? { |pattern| frame =~ pattern }
  end
  
  def self.noise_frame?(frame)
    FRAMEWORK_PATTERNS.any? { |pattern| frame =~ pattern }
  end
end

# Usage in exception handling
begin
  complex_operation
rescue => e
  cleaned_trace = BacktraceCleaner.clean(e.backtrace)
  puts "Error: #{e.message}"
  puts "Relevant stack trace:"
  cleaned_trace.first(5).each { |frame| puts "  #{frame}" }
end

Exception aggregation and pattern detection help identify recurring issues. Implement error fingerprinting based on exception type, message patterns, and stack trace similarity.

class ExceptionFingerprinter
  def self.generate_fingerprint(exception)
    signature_parts = [
      exception.class.name,
      normalize_message(exception.message),
      extract_stack_signature(exception.backtrace)
    ]
    
    Digest::MD5.hexdigest(signature_parts.join(':'))
  end
  
  private
  
  def self.normalize_message(message)
    return 'no_message' unless message
    
    # Remove variable content like IDs, timestamps, file paths
    normalized = message.dup
    normalized.gsub!(/\b\d+\b/, 'NUMBER')
    normalized.gsub!(/\b[0-9a-f-]{36}\b/, 'UUID')
    normalized.gsub!(/\/[^\s]+/, 'PATH')
    normalized.downcase
  end
  
  def self.extract_stack_signature(backtrace)
    return 'no_stack' unless backtrace
    
    # Take first 3 application frames
    app_frames = backtrace.select { |frame| BacktraceCleaner.app_frame?(frame) }
    signature_frames = app_frames.first(3)
    
    signature_frames.map do |frame|
      # Extract method and file without line numbers
      frame.gsub(/:in `.*'/, '').gsub(/:\d+/, '')
    end.join('|')
  end
end

Testing Strategies

Exception testing requires systematic verification of error conditions, recovery behaviors, and error message clarity. Ruby's testing frameworks provide specialized methods for asserting exception behavior and validating error handling paths.

RSpec offers expect { }.to raise_error() syntax for testing exceptions with detailed matching capabilities. Test both that exceptions occur and that they carry appropriate metadata and messages.

describe APIClient do
  describe '#make_request' do
    context 'when server returns 404' do
      it 'raises RequestError with status information' do
        stub_request(:get, 'http://api.example.com/resource')
          .to_return(status: 404, body: '{"error": "Not found"}')
        
        expect {
          APIClient.make_request('http://api.example.com/resource')
        }.to raise_error(APIClient::RequestError) do |error|
          expect(error.status_code).to eq('404')
          expect(error.response_body).to include('Not found')
          expect(error.request_url).to eq('http://api.example.com/resource')
        end
      end
    end
    
    context 'when network timeout occurs' do
      it 'transforms timeout to RequestError' do
        stub_request(:get, 'http://api.example.com/slow')
          .to_timeout
        
        expect {
          APIClient.make_request('http://api.example.com/slow')
        }.to raise_error(APIClient::RequestError, /timeout/i)
      end
    end
  end
end

Test exception recovery and retry logic by simulating transient failures and verifying retry attempts. Use test doubles to control failure scenarios and count retry attempts.

describe '#fetch_with_retry' do
  let(:http_client) { instance_double(Net::HTTP) }
  
  it 'retries on timeout errors up to max attempts' do
    call_count = 0
    allow(Net::HTTP).to receive(:get) do
      call_count += 1
      if call_count <= 2
        raise Net::TimeoutError, "Request timeout"
      else
        "Success response"
      end
    end
    
    result = fetch_with_retry('http://example.com', max_attempts: 3, delay: 0)
    
    expect(result).to eq("Success response")
    expect(call_count).to eq(3)
  end
  
  it 'exhausts retries and re-raises final exception' do
    allow(Net::HTTP).to receive(:get)
      .and_raise(Net::TimeoutError, "Persistent timeout")
    
    expect {
      fetch_with_retry('http://example.com', max_attempts: 2, delay: 0)
    }.to raise_error(Net::TimeoutError, "Persistent timeout")
    
    expect(Net::HTTP).to have_received(:get).exactly(2).times
  end
end

Mock external dependencies to test error boundary behavior without triggering actual failures. Create controlled error conditions that verify exception transformation and metadata preservation.

describe ErrorBoundary do
  describe '.wrap_database_errors' do
    let(:user) { double('User') }
    
    context 'when ActiveRecord::RecordNotFound occurs' do
      it 'transforms to ApplicationError with not_found category' do
        allow(User).to receive(:find).and_raise(
          ActiveRecord::RecordNotFound, "Couldn't find User with id=999"
        )
        
        expect {
          ErrorBoundary.wrap_database_errors { User.find(999) }
        }.to raise_error(ApplicationError) do |error|
          expect(error.message).to eq("Resource not found")
          expect(error.category).to eq(:not_found)
          expect(error.original_exception).to be_a(ActiveRecord::RecordNotFound)
        end
      end
    end
    
    context 'when ActiveRecord::RecordInvalid occurs' do
      it 'includes validation errors in metadata' do
        validation_errors = double('Errors', full_messages: ['Name is required', 'Email is invalid'])
        invalid_record = double('User', errors: validation_errors)
        exception = ActiveRecord::RecordInvalid.new(invalid_record)
        
        allow(user).to receive(:save!).and_raise(exception)
        
        expect {
          ErrorBoundary.wrap_database_errors { user.save! }
        }.to raise_error(ApplicationError) do |error|
          expect(error.category).to eq(:validation_error)
          expect(error.metadata[:validation_errors]).to eq(validation_errors)
          expect(error.message).to include('Name is required', 'Email is invalid')
        end
      end
    end
  end
end

Test exception fingerprinting and aggregation logic to ensure consistent error grouping. Verify that similar exceptions generate identical fingerprints while distinct errors produce different signatures.

describe ExceptionFingerprinter do
  describe '.generate_fingerprint' do
    it 'generates identical fingerprints for similar exceptions' do
      exception1 = StandardError.new("User 123 not found")
      exception1.set_backtrace(['/app/models/user.rb:45:in `find_user`'])
      
      exception2 = StandardError.new("User 456 not found")
      exception2.set_backtrace(['/app/models/user.rb:45:in `find_user`'])
      
      fingerprint1 = ExceptionFingerprinter.generate_fingerprint(exception1)
      fingerprint2 = ExceptionFingerprinter.generate_fingerprint(exception2)
      
      expect(fingerprint1).to eq(fingerprint2)
    end
    
    it 'generates different fingerprints for different exception types' do
      error1 = ArgumentError.new("Invalid input")
      error2 = StandardError.new("Invalid input")
      
      fingerprint1 = ExceptionFingerprinter.generate_fingerprint(error1)
      fingerprint2 = ExceptionFingerprinter.generate_fingerprint(error2)
      
      expect(fingerprint1).not_to eq(fingerprint2)
    end
    
    it 'handles exceptions without backtrace' do
      exception = StandardError.new("Test error")
      # No backtrace set
      
      expect {
        ExceptionFingerprinter.generate_fingerprint(exception)
      }.not_to raise_error
    end
  end
end

Production Patterns

Production exception tracking requires comprehensive monitoring, alerting, and analysis systems that capture errors without impacting application performance. Implement structured logging, error aggregation, and automated incident response workflows.

Structured exception logging formats error data as JSON or key-value pairs for efficient parsing by log aggregation systems. Include contextual information like user IDs, request IDs, and feature flags to aid in debugging production issues.

class ProductionExceptionLogger
  def self.log_exception(exception, context = {})
    log_entry = {
      timestamp: Time.current.iso8601,
      level: 'ERROR',
      exception: {
        class: exception.class.name,
        message: exception.message,
        fingerprint: ExceptionFingerprinter.generate_fingerprint(exception),
        backtrace: BacktraceCleaner.clean(exception.backtrace).first(10)
      },
      context: sanitize_context(context),
      environment: {
        hostname: Socket.gethostname,
        process_id: Process.pid,
        ruby_version: RUBY_VERSION,
        rails_env: Rails.env
      }
    }
    
    Rails.logger.error(JSON.generate(log_entry))
    
    # Send to external monitoring service
    send_to_monitoring_service(log_entry) if should_report_externally?(exception)
  end
  
  private
  
  def self.sanitize_context(context)
    sanitized = context.dup
    
    # Remove sensitive data
    sensitive_keys = [:password, :token, :secret, :api_key, :credit_card]
    sensitive_keys.each { |key| sanitized.delete(key) }
    
    # Truncate large values
    sanitized.transform_values do |value|
      case value
      when String
        value.length > 1000 ? "#{value[0, 1000]}..." : value
      when Hash
        value.size > 20 ? "#{value.size} items (truncated)" : value
      when Array
        value.size > 50 ? "#{value.size} items (truncated)" : value
      else
        value
      end
    end
  end
  
  def self.should_report_externally?(exception)
    # Don't report certain types of exceptions externally
    [ActionController::RoutingError, ActionController::InvalidAuthenticityToken]
      .none? { |type| exception.is_a?(type) }
  end
  
  def self.send_to_monitoring_service(log_entry)
    # Integration with services like Sentry, Rollbar, etc.
    ExceptionMonitoringService.report(log_entry)
  rescue => e
    # Never let monitoring failures affect application flow
    Rails.logger.warn("Failed to send exception to monitoring service: #{e.message}")
  end
end

Exception rate limiting prevents error storms from overwhelming logging systems and external monitoring services. Implement exponential backoff and circuit breaker patterns for exception reporting.

class ExceptionRateLimiter
  RATE_LIMITS = {
    default: { max_count: 100, window_seconds: 300 },      # 100 per 5 minutes
    critical: { max_count: 1000, window_seconds: 300 },     # 1000 per 5 minutes
    low_priority: { max_count: 10, window_seconds: 300 }    # 10 per 5 minutes
  }.freeze
  
  def initialize
    @counters = {}
    @mutex = Mutex.new
  end
  
  def should_report?(exception, priority: :default)
    fingerprint = ExceptionFingerprinter.generate_fingerprint(exception)
    limits = RATE_LIMITS[priority]
    
    @mutex.synchronize do
      current_time = Time.current.to_i
      window_start = current_time - limits[:window_seconds]
      
      # Clean old entries
      @counters[fingerprint] ||= []
      @counters[fingerprint].reject! { |timestamp| timestamp < window_start }
      
      # Check if under limit
      if @counters[fingerprint].size < limits[:max_count]
        @counters[fingerprint] << current_time
        true
      else
        false
      end
    end
  end
  
  def current_count(exception)
    fingerprint = ExceptionFingerprinter.generate_fingerprint(exception)
    @mutex.synchronize { @counters[fingerprint]&.size || 0 }
  end
end

# Global rate limiter instance
EXCEPTION_RATE_LIMITER = ExceptionRateLimiter.new

# Usage in exception handler
def handle_production_exception(exception, context = {})
  priority = determine_exception_priority(exception)
  
  if EXCEPTION_RATE_LIMITER.should_report?(exception, priority: priority)
    ProductionExceptionLogger.log_exception(exception, context)
  else
    # Still log locally but don't send externally
    Rails.logger.info(
      "Exception rate limited: #{exception.class} " \
      "(#{EXCEPTION_RATE_LIMITER.current_count(exception)} occurrences)"
    )
  end
end

Exception context enrichment adds relevant application state to error reports. Capture request parameters, user information, feature flags, and system metrics at the time of error occurrence.

module ExceptionContextEnricher
  extend self
  
  def enrich_context(base_context = {})
    enriched = base_context.dup
    
    # Request context
    if defined?(Rails) && Rails.application
      enriched.merge!(request_context)
    end
    
    # User context
    enriched.merge!(user_context) if current_user_available?
    
    # System context
    enriched.merge!(system_context)
    
    # Feature flags
    enriched.merge!(feature_flag_context)
    
    # Performance metrics
    enriched.merge!(performance_context)
    
    enriched
  end
  
  private
  
  def request_context
    return {} unless defined?(RequestStore) && RequestStore.store[:current_request]
    
    request = RequestStore.store[:current_request]
    {
      request: {
        id: request.uuid,
        method: request.method,
        path: request.path,
        remote_ip: request.remote_ip,
        user_agent: request.user_agent&.truncate(200),
        referer: request.referer&.truncate(200)
      }
    }
  end
  
  def user_context
    return {} unless defined?(Current) && Current.user
    
    {
      user: {
        id: Current.user.id,
        email: Current.user.email,
        role: Current.user.role,
        created_at: Current.user.created_at,
        last_active: Current.user.last_active_at
      }
    }
  end
  
  def system_context
    {
      system: {
        memory_usage: get_memory_usage,
        cpu_usage: get_cpu_usage,
        disk_usage: get_disk_usage,
        active_record_pool: get_connection_pool_status
      }
    }
  end
  
  def feature_flag_context
    return {} unless defined?(FeatureFlag)
    
    {
      feature_flags: FeatureFlag.active_flags_for_context
    }
  end
  
  def performance_context
    {
      performance: {
        request_start: RequestStore.store[:request_start_time],
        duration_ms: calculate_request_duration,
        db_query_count: db_query_count,
        cache_hit_rate: cache_hit_rate
      }
    }
  end
  
  def get_memory_usage
    `ps -o rss= -p #{Process.pid}`.to_i * 1024 rescue 0
  end
  
  def get_cpu_usage
    # Implementation depends on system monitoring tools
    0.0
  end
  
  def get_disk_usage
    statvfs = `df / | tail -1 | awk '{print $5}'`.strip rescue "0%"
    statvfs.to_i
  end
  
  def get_connection_pool_status
    return {} unless defined?(ActiveRecord)
    
    {
      size: ActiveRecord::Base.connection_pool.size,
      checked_out: ActiveRecord::Base.connection_pool.stat[:busy],
      available: ActiveRecord::Base.connection_pool.stat[:size] - ActiveRecord::Base.connection_pool.stat[:busy]
    }
  rescue
    {}
  end
end

Health check endpoints monitor exception rates and system stability. Implement application health indicators that include error rates, response times, and resource utilization metrics.

class HealthCheckController < ApplicationController
  def show
    health_data = {
      status: overall_health_status,
      timestamp: Time.current.iso8601,
      checks: {
        database: database_health,
        redis: redis_health,
        external_services: external_services_health,
        exception_rate: exception_rate_health,
        memory: memory_health,
        disk: disk_health
      },
      metrics: system_metrics
    }
    
    status_code = health_data[:status] == 'healthy' ? 200 : 503
    render json: health_data, status: status_code
  end
  
  private
  
  def overall_health_status
    checks = [
      database_health[:status],
      redis_health[:status],
      external_services_health[:status],
      exception_rate_health[:status],
      memory_health[:status]
    ]
    
    checks.all? { |status| status == 'healthy' } ? 'healthy' : 'unhealthy'
  end
  
  def exception_rate_health
    recent_error_count = count_recent_errors(window_minutes: 5)
    threshold = 50  # Max errors per 5-minute window
    
    {
      status: recent_error_count < threshold ? 'healthy' : 'unhealthy',
      recent_error_count: recent_error_count,
      threshold: threshold,
      window_minutes: 5
    }
  end
  
  def count_recent_errors(window_minutes:)
    # Implementation depends on log aggregation system
    # This example assumes structured logging with searchable timestamps
    
    since = Time.current - window_minutes.minutes
    log_query = Rails.logger.respond_to?(:search) ? 
      Rails.logger.search(level: 'ERROR', since: since) : []
    
    log_query.count
  rescue
    0  # Fail safely if log search unavailable
  end
  
  def system_metrics
    {
      ruby_version: RUBY_VERSION,
      rails_version: Rails::VERSION::STRING,
      uptime_seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i,
      process_id: Process.pid,
      thread_count: Thread.list.size,
      memory_usage_mb: (get_memory_usage / 1024 / 1024).round(2)
    }
  end
end

Reference

Core Exception Classes

Class Parent Description Usage
Exception BasicObject Root of exception hierarchy System-level exceptions
StandardError Exception Base for application errors Default rescue target
RuntimeError StandardError Generic runtime error Default for raise "message"
ArgumentError StandardError Wrong number or type of arguments Method parameter validation
NameError StandardError Undefined variable or method Missing constant/method
NoMethodError NameError Method not found Method call on wrong object
SystemExit Exception Program termination exit command
Interrupt Exception User interruption Ctrl+C signal
SignalException Exception System signal received OS signal handling

Exception Handling Keywords

Keyword Purpose Usage Notes
begin Start exception block begin...rescue...end Optional in method definitions
rescue Catch exceptions rescue ExceptionType => e Matches StandardError by default
ensure Always execute ensure...end Runs regardless of exceptions
else No exception occurred else...end Only runs if no exceptions
raise Throw exception raise ExceptionClass, "message" Re-raises current if no args
retry Restart begin block retry Use with counter to avoid infinite loops

Exception Instance Methods

Method Returns Description
#message String Exception message text
#backtrace Array<String> Stack trace as string array
#backtrace_locations Array<Thread::Backtrace::Location> Detailed location objects
#cause Exception or nil Exception that caused this one
#full_message String Formatted message with backtrace
#inspect String Object inspection string
#to_s String String representation (same as message)

Thread::Backtrace::Location Methods

Method Returns Description
#path String File path
#lineno Integer Line number
#label String Method or block name
#absolute_path String Absolute file path
#to_s String Formatted location string

Common Exception Patterns

# Basic exception handling
begin
  risky_operation
rescue SpecificError => e
  handle_specific_error(e)
rescue StandardError => e
  handle_general_error(e)
ensure
  cleanup_resources
end

# Method-level exception handling
def process_data
  # method body acts as implicit begin block
rescue ValidationError => e
  return { success: false, error: e.message }
end

# Custom exception with metadata
class ProcessingError < StandardError
  attr_reader :record_id, :step
  
  def initialize(message, record_id: nil, step: nil)
    super(message)
    @record_id = record_id
    @step = step
  end
end

# Exception transformation
begin
  external_api_call
rescue Net::TimeoutError => e
  raise ServiceUnavailableError, "External service timeout"
rescue JSON::ParserError => e
  raise DataFormatError, "Invalid response format"
end

# Retry with exponential backoff
def retry_with_backoff(max_attempts: 3)
  attempts = 0
  delay = 1
  
  begin
    attempts += 1
    yield
  rescue RetryableError => e
    if attempts < max_attempts
      sleep(delay)
      delay *= 2
      retry
    else
      raise
    end
  end
end

# Exception fingerprinting
def exception_fingerprint(exception)
  [
    exception.class.name,
    normalize_message(exception.message),
    stack_signature(exception.backtrace)
  ].join(':')
end

Testing Exception Assertions

Framework Syntax Example
RSpec expect { }.to raise_error() expect { method }.to raise_error(CustomError, /message/)
Minitest assert_raises() assert_raises(CustomError) { method }
Test::Unit assert_raise() assert_raise(CustomError) { method }

Performance Considerations

Pattern Impact Recommendation
Deep rescue blocks High CPU overhead Keep rescue logic minimal
Exception for flow control Very high overhead Use conditional logic instead
Large backtrace capture Memory overhead Limit backtrace depth
Frequent exception creation Allocation overhead Cache exception instances
Exception in tight loops Severe performance impact Validate before loop entry

Configuration Options

Setting Default Purpose
$DEBUG false Enable debug mode
$VERBOSE nil Verbosity level
Exception.to_tty? Auto-detected TTY-specific formatting
--backtrace-limit No limit CLI backtrace limitation
RUBY_BACKTRACE_LIMIT No limit Environment variable limit