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