CrackedRuby CrackedRuby

Overview

Error handling paradigms represent distinct philosophical approaches to managing exceptional conditions and failures in software systems. Each paradigm embodies different assumptions about error frequency, recovery strategies, and the relationship between error detection and program flow. The choice of paradigm affects code structure, debugging practices, system reliability, and the developer experience of both writing and maintaining error-prone code.

The primary paradigms include exception-based handling, error code returns, result types, option types, and panic/abort strategies. Exception-based systems use language-level control flow mechanisms to propagate errors up the call stack. Error code approaches return status values that callers must explicitly check. Result types encapsulate success or failure states in type-safe containers. Option types handle absence of values without representing errors. Panic strategies terminate execution when recovery is impossible.

Ruby adopts an exception-centric paradigm, providing comprehensive exception classes, structured error propagation through begin/rescue/ensure blocks, and convention-driven error signaling. This approach prioritizes clean separation between normal flow and error handling logic, automatic stack unwinding, and rich error context through exception objects.

# Exception-based paradigm in Ruby
def process_payment(amount)
  raise ArgumentError, "Amount must be positive" if amount <= 0
  raise InsufficientFundsError if account_balance < amount
  
  deduct_funds(amount)
  record_transaction(amount)
rescue InsufficientFundsError => e
  notify_insufficient_funds
  raise
rescue NetworkError => e
  retry_with_backoff(3)
end

The paradigm choice influences error granularity, composition patterns, and recovery strategies. Systems requiring fine-grained error handling may benefit from result types that force explicit handling. Applications prioritizing rapid failure detection often employ exception-based approaches. Performance-critical code paths sometimes avoid exceptions due to overhead concerns. Domain characteristics, team conventions, and language idioms guide paradigm selection.

Key Principles

Error handling paradigms rest on distinct principles about error representation, propagation, and recovery. These principles define how systems distinguish between expected exceptional conditions and programmer errors, how errors traverse program layers, and what actions constitute appropriate responses to failures.

Error vs Exception Distinction: Some paradigms differentiate between errors (expected failure conditions) and exceptions (unexpected programmer errors). Errors represent anticipated failure modes such as network timeouts, file not found, or invalid input. Exceptions indicate bugs, contract violations, or impossible states. This distinction affects whether conditions should propagate immediately or allow local recovery.

Explicit vs Implicit Propagation: Paradigms differ in whether error propagation requires explicit acknowledgment at each level. Exception-based systems propagate implicitly through stack unwinding until caught. Error code and result type approaches require explicit checking or forwarding at each call site. Implicit propagation reduces boilerplate but may hide error paths. Explicit handling increases verbosity but makes error flow visible.

Fail Fast vs Resilient Handling: Systems balance between failing immediately upon detecting errors versus attempting recovery. Fail fast approaches terminate or propagate errors quickly to prevent corrupted state. Resilient handling implements retry logic, fallbacks, and degraded operation modes. The principle applies differently across system layers—lower layers often fail fast while higher layers attempt recovery.

# Fail fast principle
def validate_configuration(config)
  raise ConfigError, "Missing required field: api_key" unless config[:api_key]
  raise ConfigError, "Invalid timeout value" unless config[:timeout].positive?
  
  config
end

# Resilient handling principle
def fetch_with_fallback(primary_url, fallback_url)
  fetch(primary_url)
rescue NetworkError
  fetch(fallback_url)
rescue => e
  return_cached_data
end

Error Context and Causality: Different paradigms provide varying mechanisms for preserving error context during propagation. Exception objects carry stack traces, error messages, and causal chains. Error codes lose context unless explicitly threaded through return values. Result types can wrap error details but require discipline to maintain context chains. Rich context aids debugging but imposes overhead.

Recoverability and Finalization: Paradigms handle resource cleanup and recovery differently. Exception systems provide finally/ensure blocks for deterministic cleanup. Error code approaches require explicit cleanup at each return point. Result types compose cleanup operations in the success/failure paths. The principle affects transaction safety, resource leak prevention, and rollback strategies.

Type System Integration: Statically typed languages integrate error handling with type checking. Result types make errors part of function signatures, enabling compile-time verification of handling. Exception specifications (where supported) declare possible exceptions. Dynamic languages like Ruby trade compile-time safety for flexibility, relying on convention and testing to ensure proper handling.

Ruby Implementation

Ruby implements exception-based error handling as the primary paradigm, built on the Exception class hierarchy and begin/rescue/ensure/else control structures. The implementation emphasizes clean separation between normal code flow and error handling logic, with exceptions propagating automatically through the call stack until caught or terminating the program.

The Exception class hierarchy organizes errors by recoverability and intent. StandardError and its descendants represent recoverable application errors that rescue blocks catch by default. More severe errors like SystemExit, SignalException, and NoMemoryError descend directly from Exception and bypass default rescue clauses. This hierarchy prevents accidental suppression of critical system errors.

# Ruby exception hierarchy usage
begin
  process_user_input(data)
rescue ArgumentError => e
  # Handles specific error type
  log_validation_error(e)
  return_error_response
rescue StandardError => e
  # Catches all standard errors
  log_error(e)
  raise  # Re-raise for higher-level handling
rescue Exception => e
  # Catches everything (dangerous - avoid in application code)
  cleanup_critical_resources
  raise
end

The begin/rescue/ensure/else structure provides comprehensive control over error handling flow. The begin block contains code that might raise exceptions. Rescue clauses catch specific exception types in order of specificity. The else clause executes only if no exceptions occur. The ensure clause always executes for cleanup, regardless of exceptions or early returns.

def read_and_process_file(path)
  file = File.open(path)
  data = file.read
  
  begin
    result = parse_json(data)
    validate_schema(result)
  rescue JSON::ParserError => e
    raise DataFormatError, "Invalid JSON in #{path}: #{e.message}"
  rescue ValidationError => e
    log_validation_failure(e)
    return default_value
  else
    # Only executes if no exceptions raised
    cache_result(result)
    result
  ensure
    # Always executes
    file.close if file
  end
end

Ruby raises exceptions with the raise method, accepting an exception class, message, and optional backtrace. The fail method is an alias for raise, sometimes used to signal programmer errors versus expected failures. Custom exception classes inherit from StandardError to enable selective rescue while remaining catchable by default handlers.

# Custom exception classes
class PaymentError < StandardError; end
class InsufficientFundsError < PaymentError; end
class PaymentGatewayError < PaymentError; end

def process_payment(amount, account)
  raise ArgumentError, "Amount must be positive" unless amount.positive?
  
  if account.balance < amount
    raise InsufficientFundsError.new("Balance #{account.balance} insufficient for #{amount}")
  end
  
  gateway_response = payment_gateway.charge(amount)
  
  unless gateway_response.success?
    raise PaymentGatewayError, "Gateway returned: #{gateway_response.error_code}"
  end
  
  gateway_response
end

Ruby supports exception chaining through the cause attribute, preserving causal relationships when rescuing and re-raising. This maintains error context across abstraction boundaries while allowing higher-level code to work with domain-appropriate exception types.

def fetch_user_profile(user_id)
  response = http_client.get("/users/#{user_id}")
  JSON.parse(response.body)
rescue HTTP::Error => e
  raise ProfileFetchError, "Failed to retrieve profile for user #{user_id}", cause: e
rescue JSON::ParserError => e
  raise ProfileFetchError, "Invalid profile data for user #{user_id}", cause: e
end

# Accessing the cause chain
begin
  profile = fetch_user_profile(123)
rescue ProfileFetchError => e
  puts "Error: #{e.message}"
  puts "Caused by: #{e.cause.message}" if e.cause
  puts "Backtrace: #{e.backtrace.first(5)}"
end

The throw/catch mechanism provides non-local control flow distinct from exception handling. While syntactically similar, throw/catch handles control flow for expected conditions like early termination of deeply nested loops, not exceptional errors. Confusing throw/catch with exception handling leads to semantic errors.

# throw/catch for control flow (not error handling)
def find_in_nested_structure(data)
  catch(:found) do
    data.each do |level1|
      level1.each do |level2|
        level2.each do |item|
          throw(:found, item) if item.matches_criteria?
        end
      end
    end
    nil  # Not found
  end
end

Ruby's retry keyword allows rescue blocks to retry the begin block, useful for transient failures like network errors. The retry jumps back to the begin statement, potentially creating infinite loops without proper limiting logic.

def fetch_with_retry(url, max_attempts: 3)
  attempts = 0
  
  begin
    attempts += 1
    http_get(url)
  rescue Timeout::Error, NetworkError => e
    if attempts < max_attempts
      sleep(2 ** attempts)  # Exponential backoff
      retry
    else
      raise
    end
  end
end

Common Patterns

Error handling patterns provide reusable strategies for common failure scenarios. These patterns address exception wrapping, retry logic, resource cleanup, and error recovery at different architectural levels.

Exception Translation Pattern wraps low-level exceptions in domain-appropriate exception types, isolating implementation details from higher layers. This pattern maintains abstraction boundaries while preserving error causality through exception chaining.

class RepositoryError < StandardError; end
class RecordNotFoundError < RepositoryError; end
class DatabaseConnectionError < RepositoryError; end

class UserRepository
  def find(user_id)
    result = database.query("SELECT * FROM users WHERE id = ?", user_id)
    raise RecordNotFoundError, "User #{user_id} not found" if result.empty?
    
    User.new(result.first)
  rescue PG::ConnectionBad => e
    raise DatabaseConnectionError, "Database unavailable", cause: e
  rescue PG::Error => e
    raise RepositoryError, "Database error: #{e.message}", cause: e
  end
end

Retry with Exponential Backoff handles transient failures by retrying operations with increasing delays. This pattern prevents overwhelming failing services while allowing recovery from temporary issues. The pattern includes maximum retry limits and exponential delay calculations.

def with_retry(max_attempts: 3, base_delay: 1, max_delay: 30)
  attempts = 0
  
  begin
    attempts += 1
    yield
  rescue *retryable_exceptions => e
    if attempts < max_attempts
      delay = [base_delay * (2 ** (attempts - 1)), max_delay].min
      sleep(delay + rand(0..delay * 0.1))  # Add jitter
      retry
    else
      raise
    end
  end
end

# Usage
with_retry(max_attempts: 5) do
  api_client.fetch_data
end

Circuit Breaker Pattern prevents cascading failures by stopping requests to failing services. The pattern tracks failure rates and transitions between closed (normal), open (failing), and half-open (testing recovery) states.

class CircuitBreaker
  STATES = [:closed, :open, :half_open].freeze
  
  def initialize(failure_threshold: 5, timeout: 60)
    @failure_threshold = failure_threshold
    @timeout = timeout
    @failure_count = 0
    @state = :closed
    @last_failure_time = nil
  end
  
  def call
    case @state
    when :open
      if Time.now - @last_failure_time > @timeout
        @state = :half_open
        attempt_call
      else
        raise CircuitOpenError, "Circuit breaker open"
      end
    when :closed, :half_open
      attempt_call
    end
  end
  
  private
  
  def attempt_call
    result = yield
    on_success
    result
  rescue => e
    on_failure
    raise
  end
  
  def on_success
    @failure_count = 0
    @state = :closed
  end
  
  def on_failure
    @failure_count += 1
    @last_failure_time = Time.now
    
    if @failure_count >= @failure_threshold
      @state = :open
    end
  end
end

Null Object Pattern handles absence of values without nil checks or exceptions. The pattern provides objects implementing expected interfaces but with neutral behavior, eliminating special-case handling.

class User
  attr_reader :name, :email
  
  def initialize(name, email)
    @name = name
    @email = email
  end
  
  def admin?
    false
  end
end

class NullUser
  def name
    "Guest"
  end
  
  def email
    nil
  end
  
  def admin?
    false
  end
end

def find_user(user_id)
  result = database.query("SELECT * FROM users WHERE id = ?", user_id)
  return NullUser.new if result.empty?
  
  User.new(result[:name], result[:email])
end

# Usage eliminates nil checks
user = find_user(123)
puts "Welcome, #{user.name}"  # No nil check needed

Result Type Pattern encapsulates success or failure states in type-safe containers, making error handling explicit in function signatures. Ruby implementations use custom classes since Ruby lacks built-in result types.

class Result
  def self.success(value)
    Success.new(value)
  end
  
  def self.failure(error)
    Failure.new(error)
  end
end

class Success < Result
  attr_reader :value
  
  def initialize(value)
    @value = value
  end
  
  def success?
    true
  end
  
  def failure?
    false
  end
  
  def map
    Result.success(yield(value))
  rescue => e
    Result.failure(e)
  end
end

class Failure < Result
  attr_reader :error
  
  def initialize(error)
    @error = error
  end
  
  def success?
    false
  end
  
  def failure?
    true
  end
  
  def map
    self
  end
end

# Usage
def parse_json(data)
  Result.success(JSON.parse(data))
rescue JSON::ParserError => e
  Result.failure(e)
end

result = parse_json(input)
result.map { |data| process_data(data) }
      .map { |processed| save_to_database(processed) }

Let It Crash Pattern allows subsystems to fail completely rather than attempting recovery, relying on supervision to restart failed components. This pattern works well in concurrent systems where isolating failures prevents corruption.

class Worker
  def perform(task)
    # No error handling - let exceptions propagate
    result = process_task(task)
    save_result(result)
  end
end

class Supervisor
  def initialize(worker_class, max_restarts: 3)
    @worker_class = worker_class
    @max_restarts = max_restarts
    @restart_count = 0
  end
  
  def supervise(task)
    worker = @worker_class.new
    worker.perform(task)
  rescue => e
    handle_worker_failure(e, task)
  end
  
  private
  
  def handle_worker_failure(error, task)
    @restart_count += 1
    
    if @restart_count < @max_restarts
      log_error("Worker failed, restarting", error)
      supervise(task)
    else
      log_error("Worker exceeded restart limit", error)
      raise
    end
  end
end

Practical Examples

Real-world error handling requires combining multiple patterns and handling complex failure scenarios across system boundaries. These examples demonstrate comprehensive approaches to common error handling challenges.

HTTP API Client with Comprehensive Error Handling manages network failures, timeouts, rate limiting, and response parsing while providing clear error messages to callers.

class APIClientError < StandardError; end
class NetworkError < APIClientError; end
class RateLimitError < APIClientError; end
class AuthenticationError < APIClientError; end
class APIResponseError < APIClientError; end

class APIClient
  def initialize(base_url, api_key, timeout: 30)
    @base_url = base_url
    @api_key = api_key
    @timeout = timeout
    @circuit_breaker = CircuitBreaker.new(failure_threshold: 3, timeout: 60)
  end
  
  def get(endpoint, params = {})
    with_retry(max_attempts: 3) do
      @circuit_breaker.call do
        response = execute_request(:get, endpoint, params)
        parse_response(response)
      end
    end
  rescue Timeout::Error => e
    raise NetworkError, "Request timeout after #{@timeout}s for #{endpoint}", cause: e
  rescue SocketError, Errno::ECONNREFUSED => e
    raise NetworkError, "Connection failed: #{e.message}", cause: e
  rescue CircuitOpenError => e
    raise NetworkError, "Service unavailable (circuit breaker open)", cause: e
  end
  
  private
  
  def execute_request(method, endpoint, params)
    uri = URI.join(@base_url, endpoint)
    uri.query = URI.encode_www_form(params) if params.any?
    
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == 'https'
    http.read_timeout = @timeout
    
    request = Net::HTTP::Get.new(uri)
    request['Authorization'] = "Bearer #{@api_key}"
    request['User-Agent'] = 'APIClient/1.0'
    
    http.request(request)
  end
  
  def parse_response(response)
    case response.code.to_i
    when 200..299
      JSON.parse(response.body)
    when 401, 403
      raise AuthenticationError, "Authentication failed: #{response.message}"
    when 429
      retry_after = response['Retry-After'].to_i
      raise RateLimitError, "Rate limit exceeded, retry after #{retry_after}s"
    when 500..599
      raise APIResponseError, "Server error: #{response.code} #{response.message}"
    else
      raise APIResponseError, "Unexpected response: #{response.code} #{response.message}"
    end
  rescue JSON::ParserError => e
    raise APIResponseError, "Invalid JSON response", cause: e
  end
  
  def with_retry(max_attempts:)
    attempts = 0
    
    begin
      attempts += 1
      yield
    rescue NetworkError, APIResponseError => e
      if attempts < max_attempts && retryable_error?(e)
        sleep(2 ** attempts)
        retry
      else
        raise
      end
    end
  end
  
  def retryable_error?(error)
    error.is_a?(NetworkError) || 
    (error.is_a?(APIResponseError) && error.message.include?('Server error'))
  end
end

Database Transaction with Rollback Handling manages transactional integrity, connection failures, and concurrent access issues while ensuring resource cleanup.

class DatabaseTransaction
  class TransactionError < StandardError; end
  class RollbackError < TransactionError; end
  class DeadlockError < TransactionError; end
  
  def initialize(connection)
    @connection = connection
  end
  
  def execute
    @connection.begin_transaction
    
    result = yield
    @connection.commit
    result
    
  rescue ActiveRecord::Rollback => e
    # Expected rollback, not an error
    @connection.rollback
    nil
    
  rescue ActiveRecord::StatementInvalid => e
    @connection.rollback
    
    if deadlock_detected?(e)
      raise DeadlockError, "Database deadlock detected", cause: e
    else
      raise TransactionError, "SQL error: #{e.message}", cause: e
    end
    
  rescue => e
    # Rollback on any unexpected error
    begin
      @connection.rollback
    rescue => rollback_error
      # Rollback itself failed - critical situation
      log_critical_error("Rollback failed after error", rollback_error)
      raise RollbackError, "Transaction rollback failed: #{rollback_error.message}", cause: e
    end
    
    raise TransactionError, "Transaction failed: #{e.message}", cause: e
    
  ensure
    # Release connection back to pool
    @connection.release if @connection.active?
  end
  
  private
  
  def deadlock_detected?(error)
    error.message.include?('deadlock') || error.message.include?('lock timeout')
  end
  
  def log_critical_error(message, error)
    # Log to monitoring system
    puts "CRITICAL: #{message} - #{error.class}: #{error.message}"
  end
end

# Usage with automatic retry on deadlock
def transfer_funds(from_account, to_account, amount, max_attempts: 3)
  attempts = 0
  
  begin
    attempts += 1
    
    DatabaseTransaction.new(ActiveRecord::Base.connection).execute do
      from_account.balance -= amount
      from_account.save!
      
      to_account.balance += amount
      to_account.save!
      
      Transaction.create!(
        from_account: from_account,
        to_account: to_account,
        amount: amount
      )
    end
    
  rescue DeadlockError => e
    if attempts < max_attempts
      sleep(rand(0.1..0.5))  # Random backoff to break deadlock cycle
      retry
    else
      raise TransferError, "Transfer failed after #{attempts} attempts due to deadlock", cause: e
    end
  end
end

File Processing Pipeline with Partial Failure Handling processes multiple files while tracking individual failures, continuing processing where possible, and generating comprehensive error reports.

class FileProcessor
  class ProcessingError < StandardError
    attr_reader :failed_files, :successful_files
    
    def initialize(message, failed_files, successful_files)
      super(message)
      @failed_files = failed_files
      @successful_files = successful_files
    end
  end
  
  def process_batch(file_paths)
    results = {
      successful: [],
      failed: [],
      errors: {}
    }
    
    file_paths.each do |path|
      begin
        process_single_file(path)
        results[:successful] << path
      rescue => e
        results[:failed] << path
        results[:errors][path] = {
          error_class: e.class.name,
          message: e.message,
          backtrace: e.backtrace.first(3)
        }
        
        # Log but continue processing other files
        log_file_error(path, e)
      end
    end
    
    if results[:failed].any?
      error_summary = generate_error_summary(results)
      raise ProcessingError.new(error_summary, results[:failed], results[:successful])
    end
    
    results[:successful]
  end
  
  private
  
  def process_single_file(path)
    file = nil
    
    begin
      validate_file_exists(path)
      validate_file_readable(path)
      
      file = File.open(path)
      data = parse_file(file)
      validate_data(data)
      transform_data(data)
      save_processed_data(data)
      
    ensure
      file&.close
    end
  end
  
  def validate_file_exists(path)
    raise ArgumentError, "File not found: #{path}" unless File.exist?(path)
  end
  
  def validate_file_readable(path)
    raise IOError, "File not readable: #{path}" unless File.readable?(path)
  end
  
  def parse_file(file)
    JSON.parse(file.read)
  rescue JSON::ParserError => e
    raise FormatError, "Invalid JSON format", cause: e
  end
  
  def validate_data(data)
    raise ValidationError, "Missing required fields" unless data['id'] && data['content']
    raise ValidationError, "Invalid data type" unless data['content'].is_a?(String)
  end
  
  def generate_error_summary(results)
    "Processed #{results[:successful].length} files successfully, #{results[:failed].length} failed:\n" +
    results[:errors].map { |path, error| "  #{path}: #{error[:error_class]} - #{error[:message]}" }.join("\n")
  end
end

Design Considerations

Selecting an error handling paradigm requires evaluating trade-offs between explicit error visibility, recovery capability, performance overhead, and code complexity. Different paradigms suit different contexts based on error frequency, system reliability requirements, and architectural constraints.

Exception-Based vs Error Code Trade-offs represent fundamental design choices. Exceptions provide automatic propagation, stack unwinding, and clean separation of error handling from normal flow. Error codes require explicit checking at each level but make error paths visible in code structure. Exceptions excel when errors are truly exceptional (rare) and require propagating through many layers. Error codes work better when errors are frequent, expected, or when explicit control flow visibility matters.

Ruby's exception-based approach assumes errors occur infrequently enough that exception overhead is acceptable. The paradigm breaks down when exceptions serve as control flow for common cases like validation or parsing, where error codes or result types perform better.

# Exception-based: clean normal flow, implicit propagation
def process_order(order_data)
  order = validate_order(order_data)  # May raise ValidationError
  payment = process_payment(order)    # May raise PaymentError
  fulfill_order(order, payment)       # May raise FulfillmentError
end

# Error code style in Ruby (uncommon but viable)
def process_order(order_data)
  order, error = validate_order(order_data)
  return [nil, error] if error
  
  payment, error = process_payment(order)
  return [nil, error] if error
  
  fulfill_order(order, payment)
end

Fail Fast vs Resilient Recovery trade safety against availability. Fail fast approaches terminate immediately upon detecting errors, preventing corrupt state propagation. Resilient strategies attempt recovery through retries, fallbacks, or degraded operation modes. Lower architectural layers typically fail fast—database layers, validation logic, core business rules. Higher layers implement resilience—API gateways retry transient failures, applications provide fallback data, user interfaces offer degraded experiences.

The decision depends on error context. Transient network failures merit retry logic. Data corruption merits immediate failure. User input errors merit validation feedback. System resource exhaustion merits graceful degradation or load shedding.

# Fail fast: validation layer
class OrderValidator
  def validate(order)
    raise ValidationError, "Missing customer" unless order.customer_id
    raise ValidationError, "Negative amount" if order.amount < 0
    raise ValidationError, "Invalid item count" if order.items.empty?
    
    order  # No recovery attempted
  end
end

# Resilient: service layer
class OrderService
  def create_order(order_data)
    order = OrderValidator.new.validate(order_data)
    
    begin
      payment_service.charge(order)
    rescue PaymentServiceUnavailable => e
      fallback_payment_provider.charge(order)
    rescue PaymentDeclined => e
      notify_customer_declined(order.customer_id)
      raise
    end
  end
end

Local vs Central Error Handling determines where error handling logic resides. Local handling places rescue blocks near error sites, providing context-specific recovery. Central handling uses global exception handlers or middleware for consistent treatment. Local handling offers flexibility but risks inconsistency. Central handling ensures uniformity but loses context.

Web applications typically combine both—controllers handle request-specific errors locally while middleware catches unhandled exceptions globally. Background job systems often centralize retry logic while workers handle domain-specific errors locally.

Checked vs Unchecked Exceptions (in languages supporting the distinction) affect whether callers must acknowledge possible exceptions. Ruby treats all exceptions as unchecked—callers need not declare or handle them. This flexibility comes at the cost of discoverability—documentation and testing become critical for understanding error conditions. Statically typed languages with checked exceptions provide compile-time verification of error handling but increase verbosity.

Error Granularity and Exception Hierarchies balance specificity against proliferation. Fine-grained exception types enable targeted handling but create maintenance burden. Coarse-grained types simplify code but reduce handling precision. Design exception hierarchies around recoverability and handling strategies, not just error sources.

# Granular hierarchy enables selective handling
class PaymentError < StandardError; end
class InsufficientFundsError < PaymentError; end
class CardExpiredError < PaymentError; end
class FraudDetectedError < PaymentError; end

def handle_payment_error(error)
  case error
  when InsufficientFundsError
    suggest_alternative_payment_method
  when CardExpiredError
    prompt_card_update
  when FraudDetectedError
    escalate_to_security_team
  else
    generic_payment_failure_handling
  end
end

Performance Considerations influence paradigm choice in performance-critical code. Exception raising and unwinding incurs overhead—creating exception objects, unwinding the stack, and populating backtraces consumes CPU cycles. Hot paths handling frequent expected failures benefit from error codes or result types. Infrequent exceptional conditions tolerate exception overhead in exchange for cleaner code structure.

Ruby exceptions are relatively expensive compared to return value checks. Profiling reveals whether exception overhead matters for specific use cases. Avoid using exceptions for flow control in tight loops or high-frequency operations.

Common Pitfalls

Error handling mistakes often stem from incomplete error coverage, incorrect recovery logic, or misuse of exception mechanisms. Understanding these pitfalls prevents subtle bugs and system failures.

Swallowing Exceptions occurs when rescue blocks catch errors without proper handling, hiding failures. Empty rescue blocks or generic handlers that log and continue execution create silent failures. This pitfall appears in code attempting to make systems "more robust" by catching everything.

# Dangerous: swallows all errors
def process_data(data)
  transform(data)
  save_to_database(data)
rescue
  # Error disappeared, no indication of failure
end

# Better: handle specifically or re-raise
def process_data(data)
  transform(data)
  save_to_database(data)
rescue ValidationError => e
  log_validation_failure(e)
  raise  # Propagate to caller
end

Catching Exception Instead of StandardError catches system-level exceptions that should not be rescued, including Interrupt, SystemExit, and SignalException. This prevents clean shutdown, breaks testing frameworks, and interferes with signal handling.

# Wrong: catches everything including system signals
begin
  run_server
rescue Exception => e
  log_error(e)
  retry  # May prevent clean shutdown
end

# Correct: catches application errors only
begin
  run_server
rescue StandardError => e
  log_error(e)
  notify_monitoring
  raise
end

Bare Retry Creates Infinite Loops when rescue blocks use retry without limiting attempts or addressing error causes. Transient failures like network errors justify retry; persistent errors like validation failures do not.

# Dangerous: infinite loop on persistent errors
def fetch_data
  begin
    api_client.get('/data')
  rescue NetworkError
    sleep(1)
    retry  # Retries forever
  end
end

# Correct: limit attempts and verify transient nature
def fetch_data(max_attempts = 3)
  attempts = 0
  
  begin
    attempts += 1
    api_client.get('/data')
  rescue NetworkError => e
    if attempts < max_attempts
      sleep(2 ** attempts)
      retry
    else
      raise
    end
  end
end

Losing Error Context During Re-raising occurs when rescue blocks raise new exceptions without preserving the original cause, breaking error chains and losing debugging information.

# Wrong: loses original error information
begin
  parse_json(data)
rescue JSON::ParserError
  raise DataError, "Failed to parse"  # Original context lost
end

# Correct: preserve cause chain
begin
  parse_json(data)
rescue JSON::ParserError => e
  raise DataError, "Failed to parse: #{e.message}", cause: e
end

Rescuing During Ensure Execution creates confusing control flow when ensure blocks contain code that might raise exceptions. Exceptions raised in ensure blocks override exceptions being propagated from the begin block.

# Problematic: cleanup error masks original error
def process_with_cleanup
  begin
    risky_operation
  ensure
    cleanup_operation  # If this raises, original error lost
  end
end

# Better: rescue within ensure to prevent masking
def process_with_cleanup
  begin
    risky_operation
  ensure
    begin
      cleanup_operation
    rescue => e
      log_error("Cleanup failed: #{e.message}")
      # Original error still propagates
    end
  end
end

Using Exceptions for Control Flow employs exceptions for non-exceptional conditions like validation, lookup failures, or iteration control. This misuse degrades performance and obscures actual errors in logs and monitoring.

# Wrong: using exceptions for expected lookup failures
def find_user(email)
  User.find_by!(email: email)
rescue ActiveRecord::RecordNotFound
  nil
end

# Correct: use methods designed for optional results
def find_user(email)
  User.find_by(email: email)
end

Inconsistent Error Handling Across Layers creates unpredictable behavior when different system layers follow different error handling conventions. Some layers might return nil, others raise exceptions, others return error codes, confusing callers about expected behavior.

# Inconsistent: repository returns nil, service raises
class UserRepository
  def find(id)
    User.find_by(id: id)  # Returns nil if not found
  end
end

class UserService
  def get_user(id)
    user = repository.find(id)
    raise NotFoundError unless user  # Inconsistent with repository
    user
  end
end

# Consistent: repository raises, service propagates
class UserRepository
  def find(id)
    User.find_by!(id: id)  # Raises if not found
  end
end

class UserService
  def get_user(id)
    repository.find(id)  # Propagates exception
  end
end

Not Validating Inputs Before Expensive Operations performs costly operations before checking inputs, wasting resources when validation fails. Validate early, fail fast for input errors before acquiring resources or performing irreversible actions.

Ignoring Exception Hierarchy catches broad exception classes when specific types would enable better handling. Catching StandardError when specific exceptions like ValidationError or NetworkError are expected prevents targeted recovery.

Reference

Exception Hierarchy

Exception Class Catchable by Default Purpose Recovery Strategy
Exception No Root of all exceptions Never rescue except for logging
StandardError Yes Recoverable application errors Rescue and handle appropriately
ArgumentError Yes Invalid arguments passed Validate inputs, fix caller
RuntimeError Yes Generic runtime error Examine context, fix root cause
IOError Yes Input/output failures Retry or provide fallback
NoMethodError Yes Method called on nil/wrong type Check object state, validate types
SystemExit No Program exit requested Allow propagation for clean exit
SignalException No System signal received Allow propagation for signal handling
Interrupt No User interrupt (Ctrl-C) Allow propagation for graceful shutdown

Control Flow Keywords

Keyword Purpose Scope Execution Timing
begin Starts exception handling block Defines protected region Executes first
rescue Catches specific exception types Handles matched exceptions After exception raised
else Executes on success only Runs if no exceptions After begin, before ensure
ensure Cleanup code that always runs Executes regardless of exceptions Always executes last
raise Raises an exception Propagates error up stack Immediate propagation
retry Retries begin block Jumps back to begin After rescue
throw/catch Non-local control flow (not errors) Control flow between blocks Immediate jump to catch

Custom Exception Patterns

Pattern Inheritance Use Case Example
Domain Errors StandardError Application-specific errors PaymentError, ValidationError
Nested Hierarchies Custom base classes Categorized error handling PaymentError > InsufficientFundsError
Error with Attributes Add attr_reader Attach error context InvalidRequestError with request_id
Cause Preservation Use cause parameter Maintain error chains Wrap low-level exceptions

Error Handling Decision Matrix

Condition Strategy Rationale
Expected failures (validation, not found) Return nil or result object Not exceptional, avoid exception overhead
Transient failures (network, timeout) Retry with exponential backoff Temporary condition, likely recoverable
Resource exhaustion (memory, connections) Fail fast, propagate immediately Cannot recover, prevent cascade
Data corruption Fail fast, log extensively Unrecoverable, needs investigation
Configuration errors Fail at startup Invalid state, fix before running
User input errors Return validation errors Expected, provide feedback
Programming errors (nil reference) Propagate exception Bug, needs fixing
External service failures Circuit breaker pattern Prevent cascade, enable recovery

Rescue Clause Ordering

Order Exception Type Reason
1 Most specific subclass Matches narrowest exception first
2 Parent classes Catches broader categories
3 StandardError Catches all application errors
Never Exception Catches system exceptions (dangerous)

Exception Raising Forms

# Message only (creates RuntimeError)
raise "Error message"

# Exception class and message
raise ArgumentError, "Invalid argument"

# Exception instance
raise CustomError.new("Message", additional_context: data)

# Re-raise current exception
raise

# With cause chain
raise NewError, "Message", cause: original_error

Ensure Clause Guarantees

Scenario Ensure Executes Exception Propagates
No exception raised Yes No
Exception raised and rescued Yes No (unless re-raised)
Exception raised, not rescued Yes Yes
Early return from begin Yes No
Exception in rescue clause Yes Yes (new exception)

Performance Characteristics

Operation Relative Cost Use When
Exception raising High Truly exceptional conditions
Exception catching Medium Expected frequency under 1%
Stack unwinding High Deep call stacks with exceptions
Return value checking Low Frequent expected failures
Result type pattern matching Low to Medium Explicit error handling needed