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