CrackedRuby logo

CrackedRuby

Error Message Format

Overview

Ruby's error message formatting system controls how exceptions and their associated information appear when displayed or logged. The system includes built-in formatting for standard exceptions, customizable backtrace display, and mechanisms for creating clear, informative error messages in applications.

The core of Ruby's error formatting revolves around the Exception class hierarchy and its #message, #backtrace, and #to_s methods. Ruby automatically formats exceptions when they propagate uncaught, but applications can also explicitly format errors for logging, debugging, or user display.

begin
  raise StandardError, "Something went wrong"
rescue => e
  puts e.class    # => StandardError
  puts e.message  # => Something went wrong
  puts e.to_s     # => Something went wrong
end

Ruby formats exceptions differently depending on context. Uncaught exceptions display with full backtraces, while rescued exceptions can be formatted selectively. The formatting includes exception class, message, and optionally the backtrace with file locations and line numbers.

def problematic_method
  raise ArgumentError, "Invalid input provided"
end

begin
  problematic_method
rescue ArgumentError => e
  # Full formatting includes class and message
  puts "#{e.class}: #{e.message}"
  # => ArgumentError: Invalid input provided
end

The formatting system also handles exception chaining through Exception#cause, allowing complex error scenarios to display hierarchical error information. This proves particularly valuable when debugging wrapped exceptions or handling errors that occur during error recovery.

Basic Usage

Ruby automatically formats uncaught exceptions with the class name, message, and full backtrace. The default format places the exception class and message at the top, followed by the backtrace showing the call stack from the point of the exception back to the program entry.

def level_three
  raise "Deep error"
end

def level_two
  level_three
end

def level_one
  level_two
end

level_one
# Output:
# script.rb:2:in `level_three': Deep error (RuntimeError)
#         from script.rb:6:in `level_two'
#         from script.rb:10:in `level_one'
#         from script.rb:13:in `<main>'

The Exception#message method returns the error message string, while Exception#to_s provides the same content but can be overridden for custom formatting. The Exception#inspect method includes both the class name and message in a formatted string.

error = StandardError.new("Custom message")
puts error.message   # => Custom message
puts error.to_s      # => Custom message
puts error.inspect   # => #<StandardError: Custom message>

Custom exception classes can override formatting methods to provide specialized error display. This allows domain-specific exceptions to present information in formats appropriate for their context.

class ValidationError < StandardError
  def initialize(field, value)
    @field = field
    @value = value
    super("Invalid #{field}: #{value}")
  end

  def to_s
    "Validation failed for #{@field} with value '#{@value}'"
  end
end

error = ValidationError.new(:email, "invalid-email")
puts error.message  # => Invalid email: invalid-email
puts error.to_s     # => Validation failed for email with value 'invalid-email'

The backtrace format includes filename, line number, method name, and the source line content when available. Ruby formats each stack frame as filename:line:in 'method_name', providing precise location information for debugging.

def calculate_average(numbers)
  raise ZeroDivisionError if numbers.empty?
  numbers.sum / numbers.length
end

calculate_average([])
# Output includes:
# script.rb:2:in `calculate_average': divided by 0 (ZeroDivisionError)

Error Handling & Debugging

Ruby provides several methods for extracting and formatting backtrace information for debugging purposes. The Exception#backtrace method returns an array of strings representing the call stack, while Exception#backtrace_locations returns an array of Thread::Backtrace::Location objects with more detailed information.

begin
  def nested_call
    raise "Debug this"
  end
  
  def middle_method
    nested_call
  end
  
  middle_method
rescue => e
  e.backtrace.each_with_index do |frame, idx|
    puts "Frame #{idx}: #{frame}"
  end
  
  # More detailed location info
  e.backtrace_locations.each do |location|
    puts "Method: #{location.label} in #{location.path}:#{location.lineno}"
  end
end

Custom exception formatting often requires controlling backtrace display depth and filtering irrelevant stack frames. Applications can truncate backtraces or highlight specific portions for clearer debugging output.

class ApplicationError < StandardError
  def formatted_backtrace(limit: 10)
    backtrace.first(limit).map.with_index do |frame, idx|
      "  #{idx + 1}. #{frame}"
    end.join("\n")
  end

  def debug_info
    <<~DEBUG
      #{self.class}: #{message}
      
      Stack trace:
      #{formatted_backtrace}
    DEBUG
  end
end

begin
  raise ApplicationError, "Something failed"
rescue ApplicationError => e
  puts e.debug_info
end

Exception chaining through Exception#cause requires careful formatting to show the complete error context. Ruby automatically sets the cause when an exception occurs during exception handling, but applications can manually chain exceptions for better error reporting.

class ConfigurationError < StandardError; end
class DatabaseError < StandardError; end

def load_config
  raise ConfigurationError, "Missing config file"
end

def connect_database
  load_config
rescue ConfigurationError => e
  raise DatabaseError, "Database connection failed due to configuration"
end

begin
  connect_database
rescue DatabaseError => e
  current = e
  depth = 0
  
  while current
    puts "#{depth == 0 ? 'Error' : 'Caused by'}: #{current.class}: #{current.message}"
    current = current.cause
    depth += 1
  end
end

Debugging complex error scenarios often requires extracting specific backtrace frames or filtering system-generated frames to focus on application code. Ruby's backtrace filtering can highlight relevant code paths while suppressing framework noise.

def filter_backtrace(backtrace, app_paths: ["lib/", "app/"])
  backtrace.select do |frame|
    app_paths.any? { |path| frame.include?(path) }
  end
end

def format_application_error(exception)
  filtered_trace = filter_backtrace(exception.backtrace)
  
  <<~ERROR
    Application Error: #{exception.message}
    
    Relevant stack frames:
    #{filtered_trace.map { |frame| "  #{frame}" }.join("\n")}
  ERROR
end

Production Patterns

Production error formatting requires balancing information richness with security concerns and log readability. Applications typically format exceptions for structured logging while avoiding exposure of sensitive information in error messages.

require 'json'
require 'logger'

class ProductionErrorFormatter
  def initialize(logger = Logger.new($stdout))
    @logger = logger
  end

  def format_exception(exception, context: {})
    error_data = {
      timestamp: Time.now.iso8601,
      error_class: exception.class.name,
      message: sanitize_message(exception.message),
      backtrace: filter_sensitive_paths(exception.backtrace),
      context: context,
      cause_chain: build_cause_chain(exception)
    }
    
    @logger.error(JSON.generate(error_data))
    error_data
  end

  private

  def sanitize_message(message)
    # Remove potentially sensitive information
    message.gsub(/password=\S+/i, 'password=[REDACTED]')
           .gsub(/token=\S+/i, 'token=[REDACTED]')
  end

  def filter_sensitive_paths(backtrace)
    return [] unless backtrace
    
    backtrace.reject { |frame| frame.include?('/gems/') }
             .first(20)  # Limit backtrace depth
  end

  def build_cause_chain(exception)
    chain = []
    current = exception.cause
    
    while current && chain.length < 5
      chain << {
        class: current.class.name,
        message: sanitize_message(current.message)
      }
      current = current.cause
    end
    
    chain
  end
end

Web applications require different error formatting for user-facing errors versus internal logging. User errors should be friendly and non-technical, while internal logs need complete diagnostic information.

class WebErrorFormatter
  def self.format_for_user(exception)
    case exception
    when ArgumentError, TypeError
      "Invalid input provided. Please check your data and try again."
    when ActiveRecord::RecordNotFound
      "The requested resource could not be found."
    when Net::TimeoutError
      "The request timed out. Please try again later."
    else
      "An unexpected error occurred. Our team has been notified."
    end
  end

  def self.format_for_logging(exception, request_id: nil)
    {
      request_id: request_id,
      timestamp: Time.now.utc.iso8601,
      error: {
        class: exception.class.name,
        message: exception.message,
        backtrace: exception.backtrace&.first(15)
      },
      environment: Rails.env,
      ruby_version: RUBY_VERSION
    }
  end
end

# Usage in Rails controller
begin
  risky_operation
rescue => e
  error_id = SecureRandom.uuid
  
  # Log detailed error
  Rails.logger.error(WebErrorFormatter.format_for_logging(e, request_id: error_id))
  
  # Return user-friendly message
  render json: {
    error: WebErrorFormatter.format_for_user(e),
    error_id: error_id
  }, status: 500
end

Monitoring systems require structured error formatting that enables alerting and analysis. Production error formatters often include additional metadata like environment information, user context, and performance metrics.

class MonitoringErrorFormatter
  def self.format_for_monitoring(exception, metadata: {})
    base_data = {
      timestamp: Time.now.to_f,
      severity: determine_severity(exception),
      error_class: exception.class.name,
      error_message: exception.message,
      fingerprint: generate_fingerprint(exception),
      stack_trace: exception.backtrace&.first(10),
      ruby_version: RUBY_VERSION,
      hostname: Socket.gethostname
    }
    
    base_data.merge(metadata)
  end

  private

  def self.determine_severity(exception)
    case exception
    when SystemExit, Interrupt
      'critical'
    when NoMemoryError, SystemStackError
      'critical'
    when ArgumentError, TypeError
      'warning'
    when StandardError
      'error'
    else
      'info'
    end
  end

  def self.generate_fingerprint(exception)
    content = "#{exception.class}:#{exception.message}"
    Digest::MD5.hexdigest(content)[0..12]
  end
end

Common Pitfalls

Exception message formatting can inadvertently expose sensitive information when exceptions include user input or system details. Messages containing passwords, tokens, or internal paths require careful sanitization.

# Problematic - exposes sensitive data
class DatabaseConnection
  def initialize(username, password, host)
    @connection_string = "postgresql://#{username}:#{password}@#{host}/db"
  end

  def connect
    raise ConnectionError, "Failed to connect with #{@connection_string}"
  end
end

# Better - sanitized error messages
class SafeDatabaseConnection
  def initialize(username, password, host)
    @username = username
    @password = password
    @host = host
  end

  def connect
    # Don't expose credentials in error message
    raise ConnectionError, "Failed to connect to database at #{@host} as #{@username}"
  end
end

Custom exception classes that override #to_s or #message can break expected formatting patterns. Applications expecting standard message formatting may not display custom-formatted exceptions correctly.

# Problematic - inconsistent with exception conventions
class WeirdError < StandardError
  def to_s
    "WEIRD ERROR HAPPENED!!!"  # Ignores message completely
  end
end

# Better - extends standard formatting
class InformativeError < StandardError
  def initialize(operation, details)
    @operation = operation
    @details = details
    super("#{operation} failed: #{details}")
  end

  def to_s
    "Operation '#{@operation}' failed with details: #{@details}"
  end
end

Backtrace formatting can consume excessive memory when exceptions occur in loops or recursive operations. Deep call stacks combined with long-running processes can lead to memory issues if backtraces are retained.

# Problematic - retains full backtraces
class ProblematicLogger
  def initialize
    @errors = []
  end

  def log_error(exception)
    @errors << {
      message: exception.message,
      backtrace: exception.backtrace,  # Full backtrace retained
      timestamp: Time.now
    }
  end
end

# Better - limits backtrace retention
class EfficientLogger
  def initialize(max_backtrace_lines: 10)
    @max_backtrace_lines = max_backtrace_lines
    @errors = []
  end

  def log_error(exception)
    @errors << {
      message: exception.message,
      backtrace: exception.backtrace&.first(@max_backtrace_lines),
      timestamp: Time.now
    }
    
    # Limit total stored errors
    @errors.shift if @errors.length > 1000
  end
end

Exception cause chains can create circular references when exceptions are improperly chained, leading to infinite loops during error formatting. This occurs when rescue blocks inadvertently reference the original exception.

# Problematic - potential circular reference
def dangerous_chaining
  begin
    raise StandardError, "Original error"
  rescue => original
    begin
      process_error(original)
    rescue => processing_error
      # Accidentally creates circular reference
      original.define_singleton_method(:cause) { processing_error }
      raise original
    end
  end
end

# Better - proper cause chaining
def safe_chaining
  begin
    raise StandardError, "Original error"
  rescue => original
    begin
      process_error(original)
    rescue => processing_error
      # Create new exception with proper cause
      raise ProcessingError.new("Error processing original error: #{processing_error.message}")
    end
  end
end

Reference

Core Exception Methods

Method Parameters Returns Description
#message None String Returns exception message string
#to_s None String Returns message, can be overridden
#inspect None String Returns formatted class and message
#backtrace None Array<String> Returns array of stack frame strings
#backtrace_locations None Array<Thread::Backtrace::Location> Returns detailed location objects
#cause None Exception or nil Returns causing exception
#full_message highlight:, order: String Returns formatted message with backtrace

Thread::Backtrace::Location Methods

Method Parameters Returns Description
#path None String Returns file path
#lineno None Integer Returns line number
#label None String Returns method name
#to_s None String Returns formatted location string

Exception Hierarchy

Exception
├── SystemExit
├── SystemStackError  
├── NoMemoryError
├── SecurityError
├── NotImplementedError
├── LoadError
├── SyntaxError
├── ScriptError
└── StandardError
    ├── RuntimeError
    ├── ArgumentError
    ├── TypeError
    ├── NameError
    ├── IndexError
    ├── KeyError
    ├── RangeError
    ├── SystemCallError
    └── IOError

Backtrace Format Patterns

Pattern Example Description
file:line:in 'method' app.rb:42:in 'calculate' Standard method call
file:line:in 'block' app.rb:15:in 'block in process' Block execution
file:line:in '<main>' script.rb:5:in '<main>' Top-level code
file:line:in '<class:Name>' model.rb:10:in '<class:User>' Class definition
file:line:in '<module:Name>' lib.rb:8:in '<module:Utils>' Module definition

Common Error Formatting Options

Context Format Choice Rationale
Development Full backtrace + message Complete debugging info
Production logs Structured JSON Machine parsing
User display Sanitized message only Security and usability
Monitoring Fingerprinted + metadata Aggregation and alerting
Testing Custom assertions Test-specific validation

Exception Cause Chain Traversal

def traverse_causes(exception)
  causes = []
  current = exception
  
  while current
    causes << {
      class: current.class.name,
      message: current.message
    }
    current = current.cause
  end
  
  causes
end

Memory-Efficient Error Storage

ERROR_FIELDS = %i[class message timestamp].freeze

def store_error_efficiently(exception)
  {
    class: exception.class.name,
    message: exception.message.to_s[0, 500],  # Truncate long messages
    backtrace_summary: exception.backtrace&.first(5),
    timestamp: Time.now.to_i
  }
end