Overview
Ruby's exception system centers on the Exception
class hierarchy and provides mechanisms for creating custom error types, managing error propagation, and implementing recovery strategies. Exception design involves creating meaningful error classes that communicate specific failure conditions and provide actionable information for debugging and error handling.
The core exception hierarchy starts with Exception
at the top, followed by StandardError
for application-level errors, and specialized classes like ArgumentError
, TypeError
, and RuntimeError
. Ruby uses the raise
keyword to throw exceptions and rescue
clauses to catch and handle them.
class CustomError < StandardError
attr_reader :error_code, :context
def initialize(message, error_code: nil, context: {})
super(message)
@error_code = error_code
@context = context
end
end
begin
raise CustomError.new("Operation failed", error_code: 500, context: {user_id: 123})
rescue CustomError => e
puts "Error #{e.error_code}: #{e.message}"
puts "Context: #{e.context}"
end
Exception design patterns include creating domain-specific error hierarchies, implementing error codes and metadata, designing for both programmatic handling and human readability, and establishing consistent error reporting across applications.
Basic Usage
Creating custom exception classes involves inheriting from StandardError
or its subclasses. The custom class can include additional attributes and methods to provide context-specific information about the error condition.
class ValidationError < StandardError
attr_reader :field, :value, :constraint
def initialize(message, field: nil, value: nil, constraint: nil)
super(message)
@field = field
@value = value
@constraint = constraint
end
def to_hash
{
error: message,
field: field,
value: value,
constraint: constraint
}
end
end
# Usage in validation context
def validate_email(email)
unless email.include?('@')
raise ValidationError.new(
"Email format invalid",
field: :email,
value: email,
constraint: "must contain @ symbol"
)
end
end
Exception hierarchies organize related errors under common parent classes. This allows rescue clauses to catch groups of related exceptions while still maintaining specific error types for detailed handling.
class DataError < StandardError; end
class ParseError < DataError; end
class FormatError < DataError; end
class CorruptionError < DataError; end
class DocumentProcessor
def process(document)
parse_headers(document)
validate_format(document)
extract_data(document)
rescue DataError => e
log_error(e)
raise ProcessingFailedError.new("Document processing failed: #{e.message}")
end
private
def parse_headers(doc)
raise ParseError.new("Invalid header structure") unless doc.has_headers?
end
def validate_format(doc)
raise FormatError.new("Unsupported document format") unless supported?(doc.format)
end
end
Error messages should provide clear information about what went wrong and how to fix it. Include relevant values, expected conditions, and suggested actions when appropriate.
class ConfigurationError < StandardError
def self.missing_key(key, config_file)
new("Missing required configuration key '#{key}' in #{config_file}. " \
"Add '#{key}: value' to the configuration file.")
end
def self.invalid_value(key, value, expected)
new("Invalid value '#{value}' for configuration key '#{key}'. " \
"Expected #{expected}.")
end
end
# Usage
def load_config(file)
config = YAML.load_file(file)
unless config['database_url']
raise ConfigurationError.missing_key('database_url', file)
end
unless config['timeout'].is_a?(Integer)
raise ConfigurationError.invalid_value('timeout', config['timeout'], 'integer')
end
config
end
Exception handling strategies include catching specific exception types, using ensure blocks for cleanup, and implementing retry logic with exponential backoff.
class RetryableOperation
MAX_RETRIES = 3
RETRY_DELAY = 1
def perform_with_retry(operation)
retries = 0
begin
operation.call
rescue TemporaryError => e
retries += 1
if retries <= MAX_RETRIES
sleep(RETRY_DELAY * retries)
retry
else
raise PermanentError.new("Operation failed after #{MAX_RETRIES} retries: #{e.message}")
end
ensure
cleanup_resources
end
end
private
def cleanup_resources
# Resource cleanup logic
end
end
Advanced Usage
Complex exception hierarchies support sophisticated error handling patterns by organizing exceptions into logical groups and providing inheritance-based polymorphism for error handling code.
module ApiErrors
class BaseError < StandardError
attr_reader :status_code, :error_code, :details
def initialize(message, status_code: 500, error_code: nil, details: {})
super(message)
@status_code = status_code
@error_code = error_code || self.class.name.demodulize.underscore
@details = details
end
def to_json(*args)
{
error: {
message: message,
code: error_code,
status: status_code,
details: details
}
}.to_json(*args)
end
end
class ClientError < BaseError
def initialize(message, status_code: 400, **kwargs)
super(message, status_code: status_code, **kwargs)
end
end
class ServerError < BaseError
def initialize(message, status_code: 500, **kwargs)
super(message, status_code: status_code, **kwargs)
end
end
class ValidationError < ClientError
attr_reader :violations
def initialize(message, violations: [], **kwargs)
super(message, status_code: 422, **kwargs)
@violations = violations
end
def add_violation(field, constraint, value = nil)
@violations << {
field: field,
constraint: constraint,
value: value
}
end
end
class AuthenticationError < ClientError
def initialize(message = "Authentication required", **kwargs)
super(message, status_code: 401, **kwargs)
end
end
class AuthorizationError < ClientError
def initialize(message = "Access denied", **kwargs)
super(message, status_code: 403, **kwargs)
end
end
end
Exception factories provide consistent exception creation across an application and can implement complex logic for determining error types and messages based on conditions.
class DatabaseErrorFactory
ERROR_CODE_MAPPINGS = {
'23505' => :unique_violation,
'23503' => :foreign_key_violation,
'23502' => :not_null_violation,
'42703' => :undefined_column
}.freeze
def self.create_from_pg_error(pg_error)
error_code = pg_error.result&.error_field(PG::Result::PG_DIAG_SQLSTATE)
error_type = ERROR_CODE_MAPPINGS[error_code]
case error_type
when :unique_violation
UniqueConstraintError.new(
"Duplicate value violates unique constraint",
constraint: extract_constraint_name(pg_error),
table: extract_table_name(pg_error)
)
when :foreign_key_violation
ForeignKeyError.new(
"Foreign key constraint violation",
constraint: extract_constraint_name(pg_error),
referenced_table: extract_referenced_table(pg_error)
)
when :not_null_violation
NotNullError.new(
"Null value in required field",
column: extract_column_name(pg_error),
table: extract_table_name(pg_error)
)
else
DatabaseError.new("Database operation failed: #{pg_error.message}")
end
end
private
def self.extract_constraint_name(error)
error.result&.error_field(PG::Result::PG_DIAG_CONSTRAINT_NAME)
end
def self.extract_table_name(error)
error.result&.error_field(PG::Result::PG_DIAG_TABLE_NAME)
end
end
Metaprogramming techniques can create dynamic exception classes and provide DSL-style error definition capabilities.
class ErrorDSL
def self.define_error_module(module_name, &block)
error_module = Module.new
const_set(module_name, error_module)
error_module.extend(ClassMethods)
error_module.module_eval(&block) if block_given?
error_module
end
module ClassMethods
def error_class(name, parent: StandardError, &block)
error_class = Class.new(parent) do
attr_reader :metadata
def initialize(message, metadata: {})
super(message)
@metadata = metadata
end
end
error_class.class_eval(&block) if block_given?
const_set(name, error_class)
# Create factory method
method_name = name.to_s.underscore.gsub('_error', '').to_sym
define_method(method_name) do |message, **kwargs|
error_class.new(message, metadata: kwargs)
end
error_class
end
def error_codes(*codes)
codes.each do |code|
error_name = "#{code.to_s.camelize}Error"
error_class(error_name.to_sym, parent: StandardError) do
define_method(:code) { code }
end
end
end
end
end
# Usage
PaymentErrors = ErrorDSL.define_error_module(:PaymentErrors) do
error_codes :insufficient_funds, :invalid_card, :expired_card, :processing_failed
error_class :RefundError do
attr_reader :transaction_id, :refund_amount
def initialize(message, transaction_id:, refund_amount:, **metadata)
super(message, metadata: metadata)
@transaction_id = transaction_id
@refund_amount = refund_amount
end
end
end
Error Handling & Debugging
Exception debugging requires comprehensive error information including stack traces, context data, and system state at the time of failure. Custom exceptions should capture relevant debugging information without exposing sensitive data.
class DebuggableError < StandardError
attr_reader :context, :timestamp, :backtrace_locations, :system_info
def initialize(message, context: {})
super(message)
@context = context.dup.freeze
@timestamp = Time.now
@backtrace_locations = caller_locations
@system_info = capture_system_info
end
def debug_info
{
error: {
class: self.class.name,
message: message,
timestamp: timestamp.iso8601
},
context: context,
system: system_info,
backtrace: formatted_backtrace
}
end
def formatted_backtrace
@backtrace_locations&.map do |location|
{
file: location.absolute_path,
line: location.lineno,
method: location.label
}
end
end
private
def capture_system_info
{
ruby_version: RUBY_VERSION,
platform: RUBY_PLATFORM,
process_id: Process.pid,
thread_id: Thread.current.object_id,
memory_usage: get_memory_usage
}
end
def get_memory_usage
if defined?(GC.stat)
GC.stat.slice(:heap_allocated_pages, :heap_live_objects, :total_allocated_objects)
else
{}
end
end
end
Error recovery patterns implement graceful degradation and fallback mechanisms when primary operations fail. Recovery strategies should be tailored to specific error types and business requirements.
class ResilientService
class ServiceError < StandardError; end
class TemporaryFailure < ServiceError; end
class PermanentFailure < ServiceError; end
def initialize(primary_service, fallback_service = nil)
@primary_service = primary_service
@fallback_service = fallback_service
@circuit_breaker = CircuitBreaker.new
end
def call_with_recovery(operation, *args)
return call_with_circuit_breaker(operation, *args) if @circuit_breaker.closed?
if @fallback_service && @circuit_breaker.open?
call_fallback(operation, *args)
else
raise ServiceError.new("Service unavailable and no fallback configured")
end
rescue TemporaryFailure => e
if @fallback_service
log_warning("Primary service failed, using fallback", error: e)
call_fallback(operation, *args)
else
raise ServiceError.new("Temporary failure and no fallback available: #{e.message}")
end
rescue PermanentFailure => e
@circuit_breaker.open!
raise ServiceError.new("Permanent failure detected: #{e.message}")
end
private
def call_with_circuit_breaker(operation, *args)
result = @primary_service.public_send(operation, *args)
@circuit_breaker.record_success
result
rescue StandardError => e
@circuit_breaker.record_failure
classify_and_reraise(e)
end
def call_fallback(operation, *args)
@fallback_service.public_send(operation, *args)
rescue StandardError => e
raise ServiceError.new("Both primary and fallback services failed: #{e.message}")
end
def classify_and_reraise(error)
case error
when Timeout::Error, Errno::ECONNREFUSED
raise TemporaryFailure.new("Network connectivity issue: #{error.message}")
when ArgumentError, NoMethodError
raise PermanentFailure.new("Configuration or interface error: #{error.message}")
else
raise TemporaryFailure.new("Unexpected error: #{error.message}")
end
end
end
Exception aggregation collects multiple errors from batch operations and presents them as a single, comprehensive error report.
class BatchProcessingError < StandardError
attr_reader :errors, :successful_items, :failed_items
def initialize(message, errors: [], successful_items: [], failed_items: [])
super(message)
@errors = errors.freeze
@successful_items = successful_items.freeze
@failed_items = failed_items.freeze
end
def summary
{
total_items: total_items,
successful_count: successful_items.length,
failed_count: failed_items.length,
error_types: error_type_summary
}
end
def detailed_report
report = ["Batch Processing Results:", ""]
report << "Successfully processed: #{successful_items.length} items"
report << "Failed to process: #{failed_items.length} items"
report << ""
if errors.any?
report << "Error Details:"
error_groups = errors.group_by(&:class)
error_groups.each do |error_class, error_list|
report << " #{error_class.name}: #{error_list.length} occurrences"
error_list.first(3).each do |error|
report << " - #{error.message}"
end
report << " ... and #{error_list.length - 3} more" if error_list.length > 3
report << ""
end
end
report.join("\n")
end
private
def total_items
successful_items.length + failed_items.length
end
def error_type_summary
errors.group_by(&:class).transform_values(&:length)
end
end
class BatchProcessor
def process_items(items)
successful_items = []
failed_items = []
errors = []
items.each_with_index do |item, index|
begin
result = process_single_item(item)
successful_items << {item: item, result: result, index: index}
rescue StandardError => e
failed_items << {item: item, index: index}
errors << e
end
end
if failed_items.any?
raise BatchProcessingError.new(
"Batch processing completed with #{failed_items.length} failures",
errors: errors,
successful_items: successful_items,
failed_items: failed_items
)
end
successful_items
end
end
Production Patterns
Production exception handling requires structured logging, error reporting, and monitoring integration. Exceptions should provide sufficient information for debugging while protecting sensitive data.
class ProductionErrorHandler
include Singleton
attr_accessor :logger, :error_reporter, :notification_service
def initialize
@logger = Rails.logger if defined?(Rails)
@error_reporter = nil
@notification_service = nil
@sensitive_fields = [:password, :api_key, :token, :secret].freeze
end
def handle_error(error, context: {})
error_id = SecureRandom.uuid
sanitized_context = sanitize_context(context)
log_error(error, error_id, sanitized_context)
report_error(error, error_id, sanitized_context)
notify_if_critical(error, error_id)
error_id
end
def wrap_operation(operation_name, context: {})
start_time = Time.current
begin
result = yield
log_success(operation_name, Time.current - start_time, context)
result
rescue StandardError => e
error_id = handle_error(e, context: context.merge(operation: operation_name))
# Re-raise with error ID for upstream handling
e.define_singleton_method(:error_id) { error_id }
raise e
end
end
private
def log_error(error, error_id, context)
return unless @logger
@logger.error({
error_id: error_id,
error_class: error.class.name,
message: error.message,
backtrace: error.backtrace&.first(10),
context: context,
timestamp: Time.current.iso8601
}.to_json)
end
def report_error(error, error_id, context)
return unless @error_reporter
@error_reporter.report(error, {
error_id: error_id,
context: context,
user_id: context[:user_id],
request_id: context[:request_id]
})
end
def notify_if_critical(error, error_id)
return unless @notification_service
return unless critical_error?(error)
@notification_service.alert(
"Critical Error: #{error.class.name}",
"Error ID: #{error_id}\nMessage: #{error.message}"
)
end
def critical_error?(error)
error.is_a?(SystemExit) ||
error.is_a?(NoMemoryError) ||
error.is_a?(SecurityError) ||
(error.respond_to?(:critical?) && error.critical?)
end
def sanitize_context(context)
return context unless context.is_a?(Hash)
context.deep_dup.tap do |sanitized|
sanitize_hash!(sanitized)
end
end
def sanitize_hash!(hash)
hash.each do |key, value|
if sensitive_field?(key)
hash[key] = '[REDACTED]'
elsif value.is_a?(Hash)
sanitize_hash!(value)
elsif value.is_a?(Array)
value.each { |item| sanitize_hash!(item) if item.is_a?(Hash) }
end
end
end
def sensitive_field?(key)
key_string = key.to_s.downcase
@sensitive_fields.any? { |field| key_string.include?(field.to_s) }
end
def log_success(operation_name, duration, context)
return unless @logger
@logger.info({
operation: operation_name,
status: 'success',
duration_ms: (duration * 1000).round(2),
context: context,
timestamp: Time.current.iso8601
}.to_json)
end
end
# Usage in Rails controller
class ApplicationController < ActionController::Base
around_action :handle_errors_with_context
private
def handle_errors_with_context
context = {
controller: controller_name,
action: action_name,
user_id: current_user&.id,
request_id: request.uuid,
ip_address: request.remote_ip
}
ProductionErrorHandler.instance.wrap_operation(
"#{controller_name}##{action_name}",
context: context
) { yield }
rescue => e
render json: {
error: "An error occurred",
error_id: e.respond_to?(:error_id) ? e.error_id : nil
}, status: 500
end
end
Health check and monitoring integration allows operations teams to detect and respond to error patterns quickly.
class ErrorMetrics
include Singleton
def initialize
@error_counts = Hash.new(0)
@error_rates = {}
@mutex = Mutex.new
@window_size = 300 # 5 minutes
@error_history = []
end
def record_error(error_class, context = {})
@mutex.synchronize do
@error_counts[error_class.name] += 1
@error_history << {
error_class: error_class.name,
timestamp: Time.current.to_f,
context: context
}
cleanup_old_errors
calculate_error_rates
end
end
def error_summary
@mutex.synchronize do
{
total_errors: @error_history.length,
error_counts: @error_counts.dup,
error_rates: @error_rates.dup,
time_window: @window_size
}
end
end
def health_check
summary = error_summary
{
status: determine_health_status(summary),
error_rate: overall_error_rate,
top_errors: top_error_classes(5),
alerts: generate_alerts(summary)
}
end
private
def cleanup_old_errors
cutoff_time = Time.current.to_f - @window_size
@error_history.reject! { |error| error[:timestamp] < cutoff_time }
# Recalculate counts
@error_counts.clear
@error_history.each do |error|
@error_counts[error[:error_class]] += 1
end
end
def calculate_error_rates
return if @error_history.empty?
time_span = [@window_size, Time.current.to_f - @error_history.first[:timestamp]].min
@error_counts.each do |error_class, count|
@error_rates[error_class] = count / time_span * 60 # errors per minute
end
end
def determine_health_status(summary)
return :healthy if summary[:total_errors] == 0
error_rate = overall_error_rate
if error_rate > 10
:critical
elsif error_rate > 5
:warning
else
:healthy
end
end
def overall_error_rate
return 0 if @error_history.empty?
time_span = [@window_size, Time.current.to_f - @error_history.first[:timestamp]].min
@error_history.length / time_span * 60
end
def top_error_classes(limit)
@error_counts.sort_by { |_, count| -count }.first(limit).to_h
end
def generate_alerts(summary)
alerts = []
if overall_error_rate > 10
alerts << "High error rate: #{overall_error_rate.round(2)} errors/minute"
end
@error_rates.each do |error_class, rate|
if rate > 5
alerts << "High #{error_class} rate: #{rate.round(2)} errors/minute"
end
end
alerts
end
end
Reference
Exception Hierarchy
Class | Inherits From | Purpose | When to Use |
---|---|---|---|
Exception |
BasicObject | Root of exception hierarchy | Never inherit directly |
StandardError |
Exception | Recoverable application errors | Base for custom exceptions |
RuntimeError |
StandardError | Generic runtime errors | Default for raise "message" |
ArgumentError |
StandardError | Invalid method arguments | Wrong argument count/type |
TypeError |
StandardError | Type-related errors | Object doesn't respond to method |
NameError |
StandardError | Name resolution failures | Undefined variable/constant |
NoMethodError |
NameError | Method not found | Method called on wrong type |
SystemExit |
Exception | Process termination | Never rescue in application code |
Interrupt |
Exception | User interrupt (Ctrl+C) | Handle for graceful shutdown |
Custom Exception Patterns
Pattern | Implementation | Use Case |
---|---|---|
Basic Custom Exception | class MyError < StandardError; end |
Simple error with custom name |
Exception with Attributes | attr_reader :code, :context in initializer |
Structured error data |
Exception Factory Methods | def self.invalid_format(value) |
Consistent error creation |
Exception Hierarchies | Multiple levels of inheritance | Grouped error handling |
Exception with Metadata | Hash or OpenStruct for additional data | Rich debugging information |
Error Handling Methods
Method | Signature | Returns | Description |
---|---|---|---|
raise |
raise(exception_class, message, backtrace) |
NoReturn | Throws exception |
rescue |
rescue ExceptionClass => var |
Block result | Catches specific exceptions |
ensure |
ensure; cleanup; end |
Block result | Always executes cleanup |
retry |
retry |
Block restart | Reruns begin block |
catch |
catch(:symbol) { throw :symbol, value } |
Thrown value | Non-local exit |
throw |
throw(:symbol, value) |
NoReturn | Jumps to catch block |
Exception Instance Methods
Method | Returns | Description |
---|---|---|
#message |
String | Exception message text |
#backtrace |
Array | Stack trace lines |
#backtrace_locations |
ArrayThread::Backtrace::Location | Detailed location objects |
#cause |
Exception or nil | Wrapped exception |
#class |
Class | Exception class |
#inspect |
String | Detailed string representation |
#to_s |
String | Message or class name |
Rescue Clause Modifiers
Pattern | Syntax | Behavior |
---|---|---|
Specific Exception | rescue ArgumentError |
Catches only ArgumentError |
Multiple Exceptions | rescue ArgumentError, TypeError |
Catches either exception type |
Exception Variable | rescue StandardError => e |
Assigns exception to variable |
Bare Rescue | rescue |
Catches StandardError and subclasses |
Rescue Modifier | expression rescue fallback |
Returns fallback on exception |
Exception Design Best Practices
Practice | Implementation | Benefit |
---|---|---|
Inherit from StandardError | class MyError < StandardError |
Allows rescue without catching system errors |
Provide Meaningful Messages | Include context and suggested fixes | Improves debugging experience |
Add Structured Data | Use attributes for error codes and metadata | Enables programmatic handling |
Create Exception Hierarchies | Group related errors under parent classes | Allows grouped exception handling |
Implement Factory Methods | Class methods that create specific error instances | Ensures consistent error creation |
Include Debugging Information | Capture relevant state and context | Facilitates production debugging |
Error Code Conventions
Code Range | Type | Usage |
---|---|---|
1000-1999 | Validation Errors | Input validation failures |
2000-2999 | Authentication Errors | Login and permission issues |
3000-3999 | Business Logic Errors | Domain rule violations |
4000-4999 | Integration Errors | External service failures |
5000-5999 | System Errors | Infrastructure and resource issues |
9000-9999 | Unknown Errors | Unexpected or unclassified errors |
Exception Safety Levels
Level | Guarantee | Implementation |
---|---|---|
No Guarantee | Operation may corrupt state | Avoid in production |
Basic Guarantee | No resource leaks, valid state | Use ensure for cleanup |
Strong Guarantee | Operation succeeds or state unchanged | Transaction-like operations |
No-Throw Guarantee | Operation never raises exceptions | Mark with documentation |