CrackedRuby logo

CrackedRuby

Fail Fast Principle

A comprehensive guide to implementing the Fail Fast principle in Ruby applications for early error detection and robust error handling.

Patterns and Best Practices Error Handling
11.4.2

Overview

The Fail Fast principle dictates that programs should detect and report errors as soon as possible rather than continuing execution with invalid state. Ruby implements this principle through exceptions, assertions, and validation mechanisms that halt execution when problems occur.

Ruby's exception system forms the core of fail fast implementation. When a method encounters invalid input or an impossible state, it raises an exception immediately instead of returning partial results or continuing with corrupted data. This approach prevents cascading failures and makes debugging significantly easier.

# Fail fast with parameter validation
def calculate_discount(price, percentage)
  raise ArgumentError, "Price must be positive" if price <= 0
  raise ArgumentError, "Percentage must be between 0-100" if percentage < 0 || percentage > 100
  
  price * (percentage / 100.0)
end

calculate_discount(-10, 50)
# => ArgumentError: Price must be positive

The principle extends beyond basic validation to include precondition checks, invariant enforcement, and early detection of resource constraints. Ruby's built-in classes demonstrate fail fast behavior throughout the standard library - array access with invalid indices, file operations on non-existent files, and method calls on nil objects all raise exceptions immediately.

# Built-in fail fast behavior
array = [1, 2, 3]
array.fetch(10)  # => IndexError: index 10 outside of array bounds

File.read("nonexistent.txt")  # => Errno::ENOENT: No such file or directory

nil.upcase  # => NoMethodError: undefined method `upcase' for nil:NilClass

The fail fast approach contrasts with defensive programming strategies that attempt to continue execution despite errors. While defensive programming might return default values or skip invalid operations, fail fast immediately signals that something has gone wrong, forcing explicit error handling and preventing silent data corruption.

Basic Usage

Parameter validation represents the most common application of fail fast principles. Methods check their inputs before performing operations, raising descriptive exceptions when parameters violate expected constraints.

class BankAccount
  def initialize(initial_balance)
    raise ArgumentError, "Initial balance cannot be negative" if initial_balance < 0
    @balance = initial_balance
  end
  
  def withdraw(amount)
    raise ArgumentError, "Withdrawal amount must be positive" if amount <= 0
    raise StandardError, "Insufficient funds" if amount > @balance
    
    @balance -= amount
  end
  
  def deposit(amount)
    raise ArgumentError, "Deposit amount must be positive" if amount <= 0
    @balance += amount
  end
end

account = BankAccount.new(100)
account.withdraw(-50)  # => ArgumentError: Withdrawal amount must be positive
account.withdraw(150)  # => StandardError: Insufficient funds

Type checking provides another layer of fail fast protection. Ruby's dynamic nature allows methods to receive unexpected object types, so explicit type validation prevents runtime errors from propagating.

def process_user_data(user_hash)
  raise TypeError, "Expected Hash, got #{user_hash.class}" unless user_hash.is_a?(Hash)
  raise KeyError, "Missing required key: email" unless user_hash.key?(:email)
  raise ArgumentError, "Email cannot be empty" if user_hash[:email].strip.empty?
  
  # Process the validated data
  user_hash[:email].downcase.strip
end

process_user_data("invalid")  # => TypeError: Expected Hash, got String
process_user_data({name: "John"})  # => KeyError: Missing required key: email

State validation ensures objects maintain valid internal state throughout their lifecycle. Methods check invariants before and after operations, preventing objects from entering inconsistent states.

class Temperature
  def initialize(celsius)
    @celsius = celsius
    validate_temperature
  end
  
  def fahrenheit
    validate_temperature
    (@celsius * 9.0 / 5.0) + 32
  end
  
  def add(degrees)
    new_temp = @celsius + degrees
    raise ArgumentError, "Temperature would be below absolute zero" if new_temp < -273.15
    @celsius = new_temp
    validate_temperature
  end
  
  private
  
  def validate_temperature
    if @celsius < -273.15
      raise StandardError, "Temperature cannot be below absolute zero: #{@celsius}°C"
    end
  end
end

temp = Temperature.new(-300)  # => StandardError: Temperature cannot be below absolute zero

Resource validation prevents operations on unavailable or corrupted resources. File operations, database connections, and network resources benefit from early validation.

class FileProcessor
  def process_file(filepath)
    raise ArgumentError, "Filepath cannot be empty" if filepath.nil? || filepath.empty?
    raise Errno::ENOENT, "File does not exist: #{filepath}" unless File.exist?(filepath)
    raise ArgumentError, "Path is a directory, not a file: #{filepath}" if File.directory?(filepath)
    raise StandardError, "File is not readable: #{filepath}" unless File.readable?(filepath)
    
    content = File.read(filepath)
    raise StandardError, "File is empty: #{filepath}" if content.empty?
    
    content.upcase
  end
end

Error Handling & Debugging

Exception hierarchies enable granular error handling while maintaining fail fast principles. Custom exception classes provide specific contexts for different failure modes, allowing calling code to handle errors appropriately.

class ValidationError < StandardError; end
class RequiredFieldError < ValidationError; end
class FormatError < ValidationError; end
class RangeError < ValidationError; end

class UserValidator
  def validate(user_data)
    validate_required_fields(user_data)
    validate_email_format(user_data[:email])
    validate_age_range(user_data[:age])
    true
  end
  
  private
  
  def validate_required_fields(data)
    required_fields = [:email, :name, :age]
    missing = required_fields.select { |field| !data.key?(field) || data[field].nil? }
    
    unless missing.empty?
      raise RequiredFieldError, "Missing required fields: #{missing.join(', ')}"
    end
  end
  
  def validate_email_format(email)
    unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
      raise FormatError, "Invalid email format: #{email}"
    end
  end
  
  def validate_age_range(age)
    unless age.is_a?(Integer) && age >= 0 && age <= 150
      raise RangeError, "Age must be between 0 and 150: #{age}"
    end
  end
end

# Specific error handling
validator = UserValidator.new
begin
  validator.validate({email: "invalid-email", name: "John"})
rescue RequiredFieldError => e
  # Handle missing fields differently
  puts "Form incomplete: #{e.message}"
rescue FormatError => e
  # Handle format errors differently  
  puts "Invalid format: #{e.message}"
rescue ValidationError => e
  # Handle all other validation errors
  puts "Validation failed: #{e.message}"
end

Assertion methods provide lightweight fail fast mechanisms for internal consistency checks. Unlike parameter validation, assertions verify assumptions about program state during development and testing.

module Assertions
  def assert(condition, message = "Assertion failed")
    raise AssertionError, message unless condition
  end
  
  def assert_positive(value, message = nil)
    message ||= "Expected positive value, got: #{value}"
    assert(value > 0, message)
  end
  
  def assert_not_nil(value, message = nil)
    message ||= "Expected non-nil value"
    assert(!value.nil?, message)
  end
end

class AssertionError < StandardError; end

class Calculator
  include Assertions
  
  def divide(dividend, divisor)
    assert_not_nil(dividend, "Dividend cannot be nil")
    assert_not_nil(divisor, "Divisor cannot be nil")
    assert(divisor != 0, "Division by zero")
    
    result = dividend.to_f / divisor
    assert_positive(result) if dividend > 0 && divisor > 0
    result
  end
end

Stack trace analysis becomes critical when fail fast exceptions occur deep in call chains. Ruby provides detailed stack traces that help identify the root cause of failures.

class DeepCallStack
  def level_one
    level_two
  end
  
  def level_two  
    level_three
  end
  
  def level_three
    raise StandardError, "Deep failure occurred"
  end
end

begin
  DeepCallStack.new.level_one
rescue StandardError => e
  puts e.message
  puts e.backtrace.first(5)  # Show first 5 stack frames
  # Output shows exact failure location and call path
end

Error context preservation maintains diagnostic information when re-raising exceptions. This pattern allows lower-level code to add context without losing the original failure details.

class DatabaseManager
  def execute_query(sql)
    # Simulate database operation
    raise StandardError, "Connection lost" if sql.include?("DROP")
    "Query result"
  end
end

class UserService
  def initialize
    @db = DatabaseManager.new
  end
  
  def delete_user(user_id)
    begin
      @db.execute_query("DELETE FROM users WHERE id = #{user_id}")
    rescue StandardError => e
      # Add context while preserving original exception
      raise StandardError, "Failed to delete user #{user_id}: #{e.message}"
    end
  end
end

class UserController
  def initialize
    @service = UserService.new
  end
  
  def destroy_user(user_id)
    begin
      @service.delete_user(user_id)
    rescue StandardError => e
      # Add controller context
      raise StandardError, "User deletion failed in controller: #{e.message}"
    end
  end
end

Common Pitfalls

Overzealous validation can make code brittle and difficult to extend. Validating implementation details rather than essential constraints creates unnecessary coupling and maintenance overhead.

# Problematic: Too specific validation
class EmailSender
  def send_email(recipient)
    # Overly restrictive - prevents future email formats
    unless recipient.match?(/\A[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}\z/i)
      raise ArgumentError, "Invalid email format"
    end
    
    # Better: Validate essential requirements only
    if recipient.nil? || recipient.empty? || !recipient.include?("@")
      raise ArgumentError, "Recipient must be a valid email address"
    end
    
    send_message(recipient)
  end
end

Exception swallowing defeats fail fast principles by catching and ignoring errors that should propagate. This anti-pattern masks problems and makes debugging extremely difficult.

# Problematic: Swallowing exceptions
class DataProcessor
  def process_files(filenames)
    results = []
    filenames.each do |filename|
      begin
        results << process_file(filename)
      rescue StandardError
        # Silently ignoring failures - bad practice
        results << nil
      end
    end
    results
  end
  
  # Better: Fail fast or explicit error handling
  def process_files_safely(filenames)
    results = []
    errors = []
    
    filenames.each do |filename|
      begin
        results << process_file(filename)
      rescue StandardError => e
        # Collect errors for proper handling
        errors << {filename: filename, error: e.message}
      end
    end
    
    unless errors.empty?
      raise StandardError, "Processing failed for files: #{errors.map { |e| e[:filename] }.join(', ')}"
    end
    
    results
  end
end

Late validation allows invalid state to persist, potentially causing cascading failures. Validation should occur at object creation and state transition points, not just before critical operations.

# Problematic: Late validation
class BankAccount
  attr_accessor :balance
  
  def initialize(balance)
    @balance = balance  # No validation here
  end
  
  def withdraw(amount)
    # Too late - invalid state may have existed for a long time
    raise StandardError, "Negative balance not allowed" if @balance < 0
    raise StandardError, "Insufficient funds" if amount > @balance
    @balance -= amount
  end
end

# Better: Early validation
class SafeBankAccount
  def initialize(balance)
    validate_balance(balance)
    @balance = balance
  end
  
  def balance=(new_balance)
    validate_balance(new_balance)
    @balance = new_balance
  end
  
  def withdraw(amount)
    validate_positive_amount(amount)
    raise StandardError, "Insufficient funds" if amount > @balance
    @balance -= amount
  end
  
  private
  
  def validate_balance(balance)
    raise ArgumentError, "Balance cannot be negative" if balance < 0
  end
  
  def validate_positive_amount(amount)
    raise ArgumentError, "Amount must be positive" if amount <= 0
  end
end

Generic exceptions reduce debugging effectiveness by providing insufficient context about failure causes. Specific exception types and detailed messages improve error diagnosis.

# Problematic: Generic exceptions
class ConfigurationManager
  def load_config(filename)
    raise StandardError, "Error" unless File.exist?(filename)  # Too vague
    raise StandardError, "Error" if File.size(filename) == 0   # Too vague
    
    config = YAML.load_file(filename)
    raise StandardError, "Error" if config.nil?                # Too vague
    
    config
  end
end

# Better: Specific exceptions with context
class ConfigurationManager
  class ConfigurationError < StandardError; end
  class FileNotFoundError < ConfigurationError; end
  class EmptyFileError < ConfigurationError; end
  class ParseError < ConfigurationError; end
  
  def load_config(filename)
    unless File.exist?(filename)
      raise FileNotFoundError, "Configuration file not found: #{filename}"
    end
    
    if File.size(filename) == 0
      raise EmptyFileError, "Configuration file is empty: #{filename}"
    end
    
    begin
      config = YAML.load_file(filename)
    rescue Psych::SyntaxError => e
      raise ParseError, "Invalid YAML syntax in #{filename}: #{e.message}"
    end
    
    if config.nil?
      raise ParseError, "Configuration file contains no valid data: #{filename}"
    end
    
    config
  end
end

Validation bypass mechanisms undermine fail fast reliability when they allow invalid operations under certain conditions. These should be used sparingly and documented clearly.

# Problematic: Inconsistent validation
class SecureProcessor  
  def process(data, skip_validation: false)
    unless skip_validation  # Dangerous bypass
      validate_data(data)
    end
    
    perform_operation(data)
  end
  
  private
  
  def validate_data(data)
    raise ArgumentError, "Data cannot be nil" if data.nil?
  end
end

# Better: Separate validated and unvalidated methods
class SecureProcessor
  def process(data)
    validate_data(data)
    perform_operation(data)
  end
  
  # Explicitly unsafe method for special cases
  def process_unvalidated(data)
    # Document why this exists and when to use it
    perform_operation(data)
  end
  
  private
  
  def validate_data(data)
    raise ArgumentError, "Data cannot be nil" if data.nil?
  end
end

Production Patterns

Health checks integrate fail fast principles into monitoring systems by immediately reporting service degradation. These checks validate that critical dependencies and internal state remain operational.

class ApplicationHealthCheck
  def initialize(database:, cache:, external_api:)
    @database = database
    @cache = cache
    @external_api = external_api
  end
  
  def healthy?
    check_database_connection
    check_cache_availability  
    check_external_api_response
    check_memory_usage
    check_disk_space
    true
  rescue HealthCheckError => e
    raise StandardError, "Application unhealthy: #{e.message}"
  end
  
  private
  
  class HealthCheckError < StandardError; end
  
  def check_database_connection
    begin
      @database.execute("SELECT 1")
    rescue StandardError => e
      raise HealthCheckError, "Database connection failed: #{e.message}"
    end
  end
  
  def check_cache_availability
    begin
      test_key = "health_check_#{Time.now.to_i}"
      @cache.set(test_key, "test_value", expires_in: 1)
      value = @cache.get(test_key)
      @cache.delete(test_key)
      
      unless value == "test_value"
        raise HealthCheckError, "Cache read/write verification failed"
      end
    rescue StandardError => e
      raise HealthCheckError, "Cache unavailable: #{e.message}"
    end
  end
  
  def check_external_api_response
    begin
      response = @external_api.get("/health", timeout: 5)
      unless response.status == 200
        raise HealthCheckError, "External API returned status #{response.status}"
      end
    rescue Net::TimeoutError
      raise HealthCheckError, "External API timeout exceeded"
    rescue StandardError => e
      raise HealthCheckError, "External API check failed: #{e.message}"
    end
  end
  
  def check_memory_usage
    memory_usage = `ps -o pid,vsz,rss -p #{Process.pid}`.split("\n").last.split.last.to_i
    memory_limit = 1_000_000  # 1GB in KB
    
    if memory_usage > memory_limit
      raise HealthCheckError, "Memory usage #{memory_usage}KB exceeds limit #{memory_limit}KB"
    end
  end
  
  def check_disk_space
    disk_usage = `df /tmp`.split("\n").last.split[4].to_i
    if disk_usage > 90
      raise HealthCheckError, "Disk usage #{disk_usage}% exceeds 90% threshold"
    end
  end
end

Circuit breaker patterns prevent cascading failures by failing fast when downstream services become unreliable. This approach protects system stability during partial outages.

class CircuitBreaker
  CLOSED = :closed
  OPEN = :open
  HALF_OPEN = :half_open
  
  def initialize(failure_threshold: 5, timeout: 60, reset_timeout: 30)
    @failure_threshold = failure_threshold
    @timeout = timeout
    @reset_timeout = reset_timeout
    @failure_count = 0
    @last_failure_time = nil
    @state = CLOSED
  end
  
  def call(&block)
    case @state
    when CLOSED
      execute_closed(&block)
    when OPEN
      execute_open(&block)  
    when HALF_OPEN
      execute_half_open(&block)
    end
  end
  
  private
  
  def execute_closed(&block)
    begin
      result = block.call
      reset_failure_count
      result
    rescue StandardError => e
      record_failure
      raise e
    end
  end
  
  def execute_open(&block)
    if Time.now - @last_failure_time >= @reset_timeout
      @state = HALF_OPEN
      execute_half_open(&block)
    else
      raise StandardError, "Circuit breaker is OPEN - failing fast"
    end
  end
  
  def execute_half_open(&block)
    begin
      result = block.call
      reset_failure_count
      @state = CLOSED
      result
    rescue StandardError => e
      record_failure
      @state = OPEN
      raise e
    end
  end
  
  def record_failure
    @failure_count += 1
    @last_failure_time = Time.now
    
    if @failure_count >= @failure_threshold
      @state = OPEN
    end
  end
  
  def reset_failure_count
    @failure_count = 0
    @last_failure_time = nil
  end
end

class ExternalServiceClient
  def initialize
    @circuit_breaker = CircuitBreaker.new(failure_threshold: 3, timeout: 30)
  end
  
  def fetch_data(id)
    @circuit_breaker.call do
      # Simulate external service call
      response = Net::HTTP.get_response(URI("https://api.example.com/data/#{id}"))
      
      unless response.code == '200'
        raise StandardError, "API request failed with status #{response.code}"
      end
      
      JSON.parse(response.body)
    end
  end
end

Request validation middleware applies fail fast principles at the application boundary, preventing invalid requests from consuming system resources.

class RequestValidationMiddleware
  def initialize(app)
    @app = app
  end
  
  def call(env)
    request = Rack::Request.new(env)
    
    validate_request_size(request)
    validate_content_type(request)
    validate_authentication(request)
    validate_rate_limits(request)
    
    @app.call(env)
  rescue ValidationError => e
    [400, {'Content-Type' => 'application/json'}, [{error: e.message}.to_json]]
  rescue AuthenticationError => e
    [401, {'Content-Type' => 'application/json'}, [{error: e.message}.to_json]]
  rescue RateLimitError => e
    [429, {'Content-Type' => 'application/json'}, [{error: e.message}.to_json]]
  end
  
  private
  
  class ValidationError < StandardError; end
  class AuthenticationError < StandardError; end
  class RateLimitError < StandardError; end
  
  def validate_request_size(request)
    max_size = 10 * 1024 * 1024  # 10MB
    content_length = request.content_length
    
    if content_length && content_length > max_size
      raise ValidationError, "Request size #{content_length} exceeds maximum #{max_size}"
    end
  end
  
  def validate_content_type(request)
    if request.post? || request.put?
      content_type = request.content_type
      
      unless content_type&.include?('application/json') || content_type&.include?('application/x-www-form-urlencoded')
        raise ValidationError, "Unsupported content type: #{content_type}"
      end
    end
  end
  
  def validate_authentication(request)
    protected_paths = ['/admin', '/api/v1/users', '/api/v1/orders']
    
    if protected_paths.any? { |path| request.path.start_with?(path) }
      auth_header = request.get_header('HTTP_AUTHORIZATION')
      
      unless auth_header&.start_with?('Bearer ')
        raise AuthenticationError, "Missing or invalid authorization header"
      end
    end
  end
  
  def validate_rate_limits(request)
    # Simplified rate limiting check
    client_ip = request.ip
    current_time = Time.now.to_i
    
    # In real implementation, use Redis or similar
    @request_counts ||= {}
    @request_counts[client_ip] ||= []
    
    # Remove requests older than 1 minute
    @request_counts[client_ip].select! { |timestamp| current_time - timestamp < 60 }
    
    if @request_counts[client_ip].length >= 100  # 100 requests per minute
      raise RateLimitError, "Rate limit exceeded for IP #{client_ip}"
    end
    
    @request_counts[client_ip] << current_time
  end
end

Resource management applies fail fast principles to prevent resource exhaustion. Database connections, file handles, and memory usage require monitoring and limits.

class ResourcePool
  def initialize(max_connections: 10, timeout: 30)
    @max_connections = max_connections
    @timeout = timeout
    @pool = []
    @checked_out = []
    @mutex = Mutex.new
  end
  
  def with_connection(&block)
    connection = checkout_connection
    begin
      block.call(connection)
    ensure
      checkin_connection(connection)
    end
  end
  
  private
  
  def checkout_connection
    @mutex.synchronize do
      # Fail fast if pool is exhausted
      if @checked_out.length >= @max_connections && @pool.empty?
        raise StandardError, "Connection pool exhausted (#{@max_connections} connections in use)"
      end
      
      connection = @pool.pop || create_connection
      @checked_out << connection
      connection
    end
  end
  
  def checkin_connection(connection)
    @mutex.synchronize do
      @checked_out.delete(connection)
      @pool << connection if connection_valid?(connection)
    end
  end
  
  def create_connection
    # Simulate connection creation with failure detection
    if Random.rand < 0.1  # 10% chance of connection failure
      raise StandardError, "Failed to create database connection"
    end
    
    OpenStruct.new(id: SecureRandom.uuid, created_at: Time.now)
  end
  
  def connection_valid?(connection)
    # Check if connection is still valid
    Time.now - connection.created_at < 3600  # 1 hour max age
  end
end

Testing Strategies

Exception testing verifies that fail fast mechanisms activate under expected conditions. Tests should validate both the exception type and message content to ensure proper error context.

require 'minitest/autorun'

class BankAccountTest < Minitest::Test
  def setup
    @account = BankAccount.new(100)
  end
  
  def test_withdraw_negative_amount_fails_fast
    error = assert_raises ArgumentError do
      @account.withdraw(-50)
    end
    assert_equal "Withdrawal amount must be positive", error.message
  end
  
  def test_withdraw_insufficient_funds_fails_fast
    error = assert_raises StandardError do
      @account.withdraw(150)
    end
    assert_equal "Insufficient funds", error.message
  end
  
  def test_negative_initial_balance_fails_fast
    error = assert_raises ArgumentError do
      BankAccount.new(-100)
    end
    assert_equal "Initial balance cannot be negative", error.message
  end
  
  def test_successful_operations_do_not_raise
    assert_nothing_raised do
      @account.deposit(50)
      @account.withdraw(25)
    end
    assert_equal 125, @account.balance
  end
end

Mock objects enable testing fail fast behavior when external dependencies are unavailable or unreliable. Mocks should simulate both success and failure conditions.

class DatabaseServiceTest < Minitest::Test
  def setup
    @mock_db = Minitest::Mock.new
    @service = DatabaseService.new(@mock_db)
  end
  
  def test_connection_failure_fails_fast
    @mock_db.expect :connect, nil do
      raise StandardError, "Connection refused"
    end
    
    error = assert_raises StandardError do
      @service.fetch_user(123)
    end
    assert_match /Database connection failed/, error.message
    @mock_db.verify
  end
  
  def test_query_timeout_fails_fast
    @mock_db.expect :connect, true
    @mock_db.expect :execute, nil, [String] do
      raise Net::TimeoutError, "Query timeout"
    end
    
    error = assert_raises StandardError do
      @service.fetch_user(123)
    end
    assert_match /Query timeout/, error.message
    @mock_db.verify
  end
  
  def test_invalid_user_id_fails_fast
    error = assert_raises ArgumentError do
      @service.fetch_user(nil)
    end
    assert_equal "User ID cannot be nil", error.message
  end
  
  def test_successful_query_returns_data
    @mock_db.expect :connect, true
    @mock_db.expect :execute, [{id: 123, name: "John"}], [String]
    
    result = @service.fetch_user(123)
    assert_equal 123, result[:id]
    assert_equal "John", result[:name]
    @mock_db.verify
  end
end

Integration tests validate fail fast behavior across system boundaries, ensuring that failures propagate correctly through multiple layers.

class UserServiceIntegrationTest < Minitest::Test
  def setup
    @database = TestDatabase.new
    @cache = TestCache.new  
    @service = UserService.new(database: @database, cache: @cache)
  end
  
  def test_database_failure_propagates
    @database.simulate_failure("Connection lost")
    
    error = assert_raises StandardError do
      @service.create_user(email: "test@example.com", name: "Test User")
    end
    assert_match /Database operation failed/, error.message
  end
  
  def test_cache_failure_does_not_prevent_operation
    @cache.simulate_failure("Cache unreachable")
    
    # Should succeed despite cache failure
    user = @service.create_user(email: "test@example.com", name: "Test User")
    assert user[:id]
  end
  
  def test_validation_failure_prevents_database_access
    database_access_count = @database.query_count
    
    assert_raises ArgumentError do
      @service.create_user(email: "", name: "Test User")  # Invalid email
    end
    
    # Database should not have been accessed due to early validation failure
    assert_equal database_access_count, @database.query_count
  end
  
  def test_complete_success_flow
    user = @service.create_user(email: "valid@example.com", name: "Valid User")
    
    assert user[:id]
    assert_equal "valid@example.com", user[:email]
    assert_equal "Valid User", user[:name]
    
    # Verify user was cached
    cached_user = @cache.get("user_#{user[:id]}")
    assert_equal user, cached_user
  end
end

class TestDatabase
  def initialize
    @should_fail = false
    @failure_message = nil
    @query_count = 0
  end
  
  def simulate_failure(message)
    @should_fail = true
    @failure_message = message
  end
  
  def query_count
    @query_count
  end
  
  def execute(sql)
    @query_count += 1
    
    if @should_fail
      raise StandardError, @failure_message
    end
    
    # Simulate successful database operation
    {id: rand(1000), created_at: Time.now}
  end
end

Stress testing validates fail fast behavior under resource pressure, ensuring that systems fail gracefully rather than hanging or corrupting data.

class ResourcePoolStressTest < Minitest::Test
  def test_pool_exhaustion_fails_fast
    pool = ResourcePool.new(max_connections: 2)
    connections = []
    
    # Exhaust the pool
    2.times do
      pool.with_connection { |conn| connections << conn }
    end
    
    # Next request should fail immediately
    start_time = Time.now
    error = assert_raises StandardError do
      pool.with_connection { |conn| sleep 1 }
    end
    end_time = Time.now
    
    assert_match /Connection pool exhausted/, error.message
    # Should fail immediately, not after timeout
    assert (end_time - start_time) < 0.1
  end
  
  def test_concurrent_access_fails_fast_appropriately
    pool = ResourcePool.new(max_connections: 3)
    errors = []
    successes = []
    
    threads = 10.times.map do
      Thread.new do
        begin
          pool.with_connection do |conn|
            sleep 0.1  # Hold connection briefly
            successes << conn
          end
        rescue StandardError => e
          errors << e
        end
      end
    end
    
    threads.each(&:join)
    
    # Some requests should succeed, others should fail fast
    assert successes.length > 0
    assert errors.length > 0
    
    errors.each do |error|
      assert_match /Connection pool exhausted/, error.message
    end
  end
end

Reference

Core Exception Classes

Exception Class Use Case Example
ArgumentError Invalid method parameters raise ArgumentError, "Value must be positive"
TypeError Incorrect object type raise TypeError, "Expected String, got #{obj.class}"
StandardError General application errors raise StandardError, "Operation failed"
RuntimeError Runtime state violations raise RuntimeError, "Invalid state detected"
KeyError Missing hash keys raise KeyError, "Required key missing: #{key}"
IndexError Array/string bounds violations raise IndexError, "Index #{i} out of bounds"

Validation Patterns

Pattern Implementation Use Case
Parameter validation raise ArgumentError unless condition Method entry points
Type checking raise TypeError unless obj.is_a?(Class) Dynamic typing safety
State validation raise StandardError unless valid_state? Object invariants
Precondition checks raise StandardError, "Precondition failed" unless condition Method assumptions
Resource validation raise Errno::ENOENT unless File.exist?(path) External resources
Range validation raise RangeError unless (min..max).include?(value) Numeric bounds

Custom Exception Hierarchy

# Base application exception
class ApplicationError < StandardError; end

# Validation failures
class ValidationError < ApplicationError; end
class RequiredFieldError < ValidationError; end
class FormatError < ValidationError; end
class RangeError < ValidationError; end

# System failures
class SystemError < ApplicationError; end
class DatabaseError < SystemError; end
class NetworkError < SystemError; end
class ConfigurationError < SystemError; end

# Resource failures
class ResourceError < ApplicationError; end  
class ResourceNotFoundError < ResourceError; end
class ResourceExhaustedError < ResourceError; end
class ResourceCorruptedError < ResourceError; end

Assertion Methods

Method Parameters Returns Description
assert(condition, message) condition (Boolean), message (String) nil Raises AssertionError if condition is false
assert_nil(value, message) value (Object), message (String) nil Raises AssertionError if value is not nil
assert_not_nil(value, message) value (Object), message (String) nil Raises AssertionError if value is nil
assert_equal(expected, actual, message) expected (Object), actual (Object), message (String) nil Raises AssertionError if values not equal
assert_instance_of(class, obj, message) class (Class), obj (Object), message (String) nil Raises AssertionError if obj not instance of class

Circuit Breaker States

State Behavior Transitions
CLOSED Normal operation, failures counted To OPEN when failure threshold exceeded
OPEN All requests fail fast To HALF_OPEN after reset timeout
HALF_OPEN Single test request allowed To CLOSED on success, to OPEN on failure

Error Handling Decision Matrix

Error Type Immediate Action Recovery Strategy Logging Level
Validation errors Raise specific exception Request correction INFO
Resource unavailable Raise with retry suggestion Exponential backoff WARN
Configuration errors Raise at startup Manual intervention ERROR
System failures Raise with context Circuit breaker activation ERROR
Authentication failures Raise immediately User re-authentication WARN
Authorization failures Raise with audit log Permission review WARN

Testing Exception Patterns

# Assert specific exception type and message
error = assert_raises(ExceptionClass) { dangerous_operation }
assert_equal "Expected message", error.message

# Assert exception with pattern matching
error = assert_raises(StandardError) { operation }
assert_match /pattern/, error.message

# Assert no exception raised
assert_nothing_raised { safe_operation }

# Assert exception cause chain
begin
  operation_that_wraps_exceptions
rescue => e
  assert_instance_of OriginalError, e.cause
end

Health Check Templates

# Basic health check structure
def healthy?
  check_dependencies
  check_resources  
  check_configuration
  check_internal_state
  true
rescue HealthCheckError => e
  raise StandardError, "Unhealthy: #{e.message}"
end

# Dependency validation
def check_dependencies
  dependencies.each do |name, checker|
    checker.call
  rescue => e
    raise HealthCheckError, "#{name} failed: #{e.message}"
  end
end