Ruby Exception Handling
Exception handling is an essential part of writing robust programs. During program execution, various unexpected situations may occur, such as file not found, network connection failure, invalid input, etc. Ruby provides a complete exception handling mechanism to help developers gracefully handle these situations, improving program stability and user experience. This chapter will provide a detailed introduction to various exception handling methods and best practices in Ruby.
🎯 Exception Basics
What is an Exception
An exception is an abnormal situation that occurs during program execution, which interrupts the normal flow of the program. Exceptions in Ruby are objects that inherit from the Exception class or its subclasses.
# Basic exception handling
begin
# Code that might error
result = 10 / 0
rescue
# Handle exception
puts "An error occurred!"
end
# More specific exception handling
begin
result = 10 / 0
rescue ZeroDivisionError
puts "Cannot divide by zero!"
end
# Capture exception object
begin
result = 10 / 0
rescue ZeroDivisionError => e
puts "Error type: #{e.class}"
puts "Error message: #{e.message}"
puts "Error backtrace: #{e.backtrace}"
endRuby Exception Hierarchy
# Ruby exception class hierarchy (partial)
# Exception
# ├── NoMemoryError
# ├── ScriptError
# │ ├── LoadError
# │ ├── NotImplementedError
# │ └── SyntaxError
# ├── SignalException
# │ └── Interrupt
# ├── StandardError # Most commonly used exception base class
# │ ├── ArgumentError
# │ ├── IOError
# │ ├── IndexError
# │ ├── LocalJumpError
# │ ├── NameError
# │ │ └── NoMethodError
# │ ├── RangeError
# │ ├── RegexpError
# │ ├── RuntimeError
# │ ├── SecurityError
# │ ├── StandardError
# │ ├── StopIteration
# │ ├── SyntaxError
# │ ├── SystemCallError
# │ ├── SystemStackError
# │ ├── ThreadError
# │ ├── TypeError
# │ └── ZeroDivisionError
# ├── SystemExit
# └── fatal
# Common exception type examples
begin
# ArgumentError - argument error
def greet(name)
raise ArgumentError, "Name cannot be empty" if name.nil? || name.empty?
puts "Hello, #{name}!"
end
greet("") # Will throw ArgumentError
rescue ArgumentError => e
puts "Argument error: #{e.message}"
end
begin
# TypeError - type error
result = "hello" + 5 # Will throw TypeError
rescue TypeError => e
puts "Type error: #{e.message}"
end
begin
# NoMethodError - method doesn't exist error
obj = "hello"
obj.nonexistent_method # Will throw NoMethodError
rescue NoMethodError => e
puts "Method error: #{e.message}"
end🚨 Exception Handling Syntax
Basic Exception Handling Structure
# Complete exception handling structure
begin
# Code that might error
puts "Start execution"
raise "This is a custom error"
puts "This line will not execute"
rescue StandardError => e
# Handle standard exceptions
puts "Caught exception: #{e.message}"
rescue Exception => e
# Handle all exceptions (not recommended)
puts "Caught all exceptions: #{e.message}"
else
# Execute when no exception occurs
puts "No exception occurred"
ensure
# Execute regardless of whether exception occurred
puts "Cleanup work"
endMultiple Exception Handling
# Handle multiple exception types
begin
# Code that might throw different exceptions
file = File.open("nonexistent.txt")
data = file.read
result = 10 / 0
rescue Errno::ENOENT => e
# File not found error
puts "File not found: #{e.message}"
rescue ZeroDivisionError => e
# Division by zero error
puts "Division by zero error: #{e.message}"
rescue IOError => e
# IO error
puts "IO error: #{e.message}"
rescue StandardError => e
# Other standard errors
puts "Other error: #{e.message}"
end
# Use array to handle multiple exceptions
begin
# Code that might error
raise ArgumentError, "Argument error"
rescue [ArgumentError, TypeError, NameError] => e
puts "Caught type-related error: #{e.class} - #{e.message}"
endException Re-raising and Nesting
# Re-raise exception
begin
begin
raise "Inner error"
rescue => e
puts "Internal catch: #{e.message}"
# Re-raise exception
raise
end
rescue => e
puts "External catch: #{e.message}"
end
# Throw different exception
begin
begin
raise "Original error"
rescue => e
puts "Caught original error: #{e.message}"
# Throw new exception
raise RuntimeError, "Transformed error"
end
rescue RuntimeError => e
puts "Caught transformed error: #{e.message}"
end
# Exception chaining (Ruby 2.1+)
begin
begin
raise "Original error"
rescue => e
raise "New error", cause: e
end
rescue => e
puts "New error: #{e.message}"
puts "Original error: #{e.cause.message}" if e.cause
end🏗️ Custom Exceptions
Creating Custom Exception Classes
# Basic custom exception
class CustomError < StandardError
end
# Custom exception with constructor
class ValidationError < StandardError
attr_reader :field, :value
def initialize(field, value, message = nil)
@field = field
@value = value
message ||= "Field '#{field}' value '#{value}' is invalid"
super(message)
end
end
# Using custom exception
def validate_age(age)
raise ValidationError.new("age", age) if age < 0 || age > 150
true
end
begin
validate_age(-5)
rescue ValidationError => e
puts "Validation error: #{e.message}"
puts "Field: #{e.field}, Value: #{e.value}"
end
# Hierarchical custom exceptions
class ApplicationError < StandardError
end
class DatabaseError < ApplicationError
attr_reader :query
def initialize(query, message = nil)
@query = query
message ||= "Database query failed: #{query}"
super(message)
end
end
class NetworkError < ApplicationError
attr_reader :url
def initialize(url, message = nil)
@url = url
message ||= "Network request failed: #{url}"
super(message)
end
end
# Using hierarchical exceptions
def fetch_data(url)
# Simulate network request
if url.include?("error")
raise NetworkError.new(url, "Connection timeout")
end
"Data content"
end
begin
data = fetch_data("http://example.com/error")
rescue ApplicationError => e
puts "Application error: #{e.message}"
case e
when NetworkError
puts "Network error, URL: #{e.url}"
when DatabaseError
puts "Database error, query: #{e.query}"
end
end🔧 Exception-Related Methods
Throwing Exceptions
# Use raise to throw exception
raise "This is an error message"
raise RuntimeError, "Runtime error"
raise RuntimeError.new("Custom runtime error")
# Use fail to throw exception (same as raise)
fail "This is also an error message"
fail ArgumentError, "Argument error"
# Throw specific exception object
exception = ArgumentError.new("Invalid argument")
raise exception
# Re-throw current exception
begin
raise "Original error"
rescue
puts "Processing..."
raise # Re-throw
endException Checking and Handling
# Check if exception can be caught
def safe_operation
catch(:done) do
# Operations that might throw exceptions
throw :done, "Operation completed"
end
rescue => e
"Error occurred: #{e.message}"
end
# Use retry for retry
attempt = 0
begin
attempt += 1
puts "Attempt ##{attempt}"
raise "Simulated error" if attempt < 3
puts "Success!"
rescue => e
if attempt < 3
puts "Retrying..."
retry
else
puts "Final failure: #{e.message}"
end
endException Information Access
begin
raise "Custom error message"
rescue => e
puts "Exception class: #{e.class}"
puts "Exception message: #{e.message}"
puts "Exception backtrace:"
e.backtrace.each { |line| puts " #{line}" }
# Get detailed exception information
puts "Exception details: #{e.inspect}"
# Get cause (Ruby 2.1+)
puts "Exception cause: #{e.cause}" if e.cause
end🎯 Practical Exception Handling Examples
File Operation Exception Handling
class FileProcessor
# Safe file reading
def self.safe_read(filename)
File.open(filename, "r") do |file|
file.read
end
rescue Errno::ENOENT
puts "Error: File '#{filename}' does not exist"
nil
rescue Errno::EACCES
puts "Error: No permission to read file '#{filename}'"
nil
rescue => e
puts "Unknown error occurred while reading file: #{e.message}"
nil
end
# Safe file writing
def self.safe_write(filename, content)
File.open(filename, "w") do |file|
file.write(content)
end
true
rescue Errno::EACCES
puts "Error: No permission to write file '#{filename}'"
false
rescue => e
puts "Unknown error occurred while writing file: #{e.message}"
false
end
# Process file and return result
def self.process_file(filename)
content = safe_read(filename)
return nil unless content
# Process content
processed_content = content.upcase
# Write new file
output_filename = "#{filename}.processed"
safe_write(output_filename, processed_content)
output_filename
end
end
# Using file processor
# result = FileProcessor.process_file("example.txt")
# if result
# puts "File processing completed: #{result}"
# else
# puts "File processing failed"
# endNetwork Request Exception Handling
require 'net/http'
require 'uri'
class NetworkClient
class NetworkError < StandardError
attr_reader :url, :status_code
def initialize(url, status_code = nil, message = nil)
@url = url
@status_code = status_code
message ||= status_code ?
"HTTP #{status_code} error: #{url}" :
"Network request failed: #{url}"
super(message)
end
end
class TimeoutError < NetworkError
def initialize(url, message = nil)
super(url, nil, message || "Request timeout: #{url}")
end
end
# Send GET request
def self.get(url, timeout: 5)
uri = URI(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.open_timeout = timeout
http.read_timeout = timeout
response = http.get(uri.request_uri)
case response
when Net::HTTPSuccess
response.body
else
raise NetworkError.new(url, response.code.to_i)
end
rescue Net::OpenTimeout, Net::ReadTimeout
raise TimeoutError.new(url)
rescue SocketError
raise NetworkError.new(url, nil, "DNS resolution failed: #{url}")
rescue => e
raise NetworkError.new(url, nil, "Unknown network error: #{e.message}")
end
# Safe GET request
def self.safe_get(url, timeout: 5)
get(url, timeout: timeout)
rescue NetworkError => e
puts "Network error: #{e.message}"
nil
end
end
# Using network client
# response = NetworkClient.safe_get("https://api.github.com/users/octocat")
# if response
# puts "Response content: #{response[0..100]}..."
# else
# puts "Request failed"
# endData Validation Exception Handling
class DataValidator
class ValidationError < StandardError
attr_reader :field, :value, :constraint
def initialize(field, value, constraint, message = nil)
@field = field
@value = value
@constraint = constraint
message ||= "Field '#{field}' value '#{value}' does not satisfy constraint '#{constraint}'"
super(message)
end
end
# Validate required fields
def self.validate_required(data, field)
value = data[field]
raise ValidationError.new(field, value, "required",
"Field '#{field}' is required") if value.nil? || value.to_s.empty?
value
end
# Validate numeric range
def self.validate_range(data, field, min, max)
value = data[field]
raise ValidationError.new(field, value, "range(#{min}-#{max})",
"Field '#{field}' must be between #{min} and #{max}") unless value.is_a?(Numeric) && value.between?(min, max)
value
end
# Validate email format
def self.validate_email(data, field)
value = data[field]
email_pattern = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
raise ValidationError.new(field, value, "email",
"Field '#{field}' must be a valid email address") unless value.to_s.match?(email_pattern)
value
end
# Batch validation
def self.validate(data, rules)
errors = []
rules.each do |field, rule|
begin
case rule[:type]
when :required
validate_required(data, field)
when :range
validate_range(data, field, rule[:min], rule[:max])
when :email
validate_email(data, field)
end
rescue ValidationError => e
errors << e
end
end
unless errors.empty?
raise ValidationError.new("multiple", nil, "multiple",
"Validation failed, #{errors.length} errors in total")
end
true
end
end
# Using data validator
# user_data = {
# name: "Alice",
# age: 25,
# email: "alice@example.com"
# }
#
# validation_rules = {
# name: { type: :required },
# age: { type: :range, min: 0, max: 150 },
# email: { type: :email }
# }
#
# begin
# DataValidator.validate(user_data, validation_rules)
# puts "Data validation passed"
# rescue DataValidator::ValidationError => e
# puts "Validation error: #{e.message}"
# end📊 Exception Handling Performance Optimization
Exception Handling Performance Considerations
# Avoid using exceptions to control normal flow
# Not recommended: use exception to control flow
def find_user_bad(id)
begin
User.find(id)
rescue ActiveRecord::RecordNotFound
nil
end
end
# Recommended: use normal method
def find_user_good(id)
User.find_by(id: id)
end
# Pre-check to avoid exceptions
def safe_divide(dividend, divisor)
# Pre-check
return nil if divisor == 0
dividend / divisor
end
# Use rescue modifier for simple cases
def safe_parse_integer(string)
Integer(string)
rescue ArgumentError
nil
end
# Exception handling in batch operations
def process_items(items)
results = []
errors = []
items.each_with_index do |item, index|
begin
result = process_item(item)
results << { index: index, item: item, result: result }
rescue => e
errors << { index: index, item: item, error: e.message }
end
end
{ results: results, errors: errors }
end
def process_item(item)
# Process single item
item.upcase
endException Logging
require 'logger'
class ExceptionLogger
def initialize(log_file = "error.log")
@logger = Logger.new(log_file)
@logger.level = Logger::ERROR
end
# Log exception
def log_exception(e, context = {})
error_info = {
exception_class: e.class.name,
message: e.message,
backtrace: e.backtrace&.first(10),
context: context,
timestamp: Time.now
}
@logger.error(error_info.to_json)
end
# Exception handling with context
def handle_with_context(context = {})
yield
rescue => e
log_exception(e, context)
raise # Re-throw exception
end
end
# Using exception logger
# logger = ExceptionLogger.new("app_errors.log")
#
# logger.handle_with_context(user_id: 123, action: "update_profile") do
# # Operations that might error
# update_user_profile(user_id: 123, data: profile_data)
# end🛡️ Exception Handling Best Practices
1. Exception Handling Principles
# Principle 1: Specific exception handling
# Bad practice
begin
# Multiple possible errors
file = File.open("data.txt")
data = JSON.parse(file.read)
result = 10 / data["divisor"]
rescue => e
puts "Error occurred: #{e.message}"
end
# Good practice
begin
file = File.open("data.txt")
data = JSON.parse(file.read)
result = 10 / data["divisor"]
rescue Errno::ENOENT => e
puts "File not found: #{e.message}"
rescue JSON::ParserError => e
puts "JSON parsing error: #{e.message}"
rescue ZeroDivisionError => e
puts "Division by zero error: #{e.message}"
rescue => e
puts "Unknown error: #{e.message}"
end
# Principle 2: Don't ignore exceptions
# Bad practice
begin
risky_operation
rescue
# Do nothing
end
# Good practice
begin
risky_operation
rescue => e
log_error(e)
# Or re-throw
raise
end
# Principle 3: Clean up resources
# Good practice
file = nil
begin
file = File.open("data.txt")
process_file(file)
ensure
file&.close # Ensure file is closed
end
# Better practice
File.open("data.txt") do |file|
process_file(file)
end # File automatically closed2. Custom Exception Design
# Well-designed exception hierarchy
module MyApp
class Error < StandardError
attr_reader :context
def initialize(message = nil, context = {})
@context = context
super(message)
end
end
class ValidationError < Error
attr_reader :field, :value
def initialize(field, value, message = nil, context = {})
@field = field
@value = value
message ||= "Field '#{field}' validation failed"
super(message, context)
end
end
class BusinessLogicError < Error
attr_reader :code
def initialize(code, message = nil, context = {})
@code = code
message ||= "Business logic error: #{code}"
super(message, context)
end
end
class ExternalServiceError < Error
attr_reader :service, :http_status
def initialize(service, http_status = nil, message = nil, context = {})
@service = service
@http_status = http_status
message ||= http_status ?
"#{service} service error (HTTP #{http_status})" :
"#{service} service error"
super(message, context)
end
end
end
# Using custom exceptions
begin
raise MyApp::ValidationError.new("email", "invalid-email",
"Invalid email format", { user_id: 123 })
rescue MyApp::ValidationError => e
puts "Validation error: #{e.message}"
puts "Field: #{e.field}, Value: #{e.value}"
puts "User ID: #{e.context[:user_id]}"
end3. Exception Handling Patterns
# Retry pattern
class RetryableOperation
def self.execute(max_retries: 3, base_delay: 1)
retries = 0
begin
yield
rescue => e
retries += 1
if retries <= max_retries
delay = base_delay * (2 ** (retries - 1)) # Exponential backoff
puts "Retry ##{retries}, waiting #{delay} seconds..."
sleep(delay)
retry
else
puts "Failed after #{max_retries} retries: #{e.message}"
raise
end
end
end
end
# Using retry pattern
# RetryableOperation.execute(max_retries: 3) do
# # Operation that might fail
# unstable_network_call
# end
# Circuit breaker pattern
class CircuitBreaker
def initialize(threshold: 5, timeout: 60)
@threshold = threshold
@timeout = timeout
@failure_count = 0
@last_failure_time = nil
@open = false
end
def call
return yield if closed?
if timeout_expired?
@open = false
@failure_count = 0
return yield
end
raise "Circuit breaker is open"
end
def record_failure
@failure_count += 1
@last_failure_time = Time.now
@open = @failure_count >= @threshold
end
private
def closed?
!@open
end
def timeout_expired?
@last_failure_time && (Time.now - @last_failure_time) > @timeout
end
end
# Using circuit breaker
# breaker = CircuitBreaker.new(threshold: 3, timeout: 30)
#
# begin
# breaker.call do
# external_service_call
# end
# rescue => e
# breaker.record_failure
# raise
# end4. Real Application Examples
# Web application controller exception handling
class ApplicationController
def handle_request
begin
# Process request
process_request
rescue ValidationError => e
render json: { error: "Validation error", details: e.message }, status: 400
rescue RecordNotFound => e
render json: { error: "Resource not found", details: e.message }, status: 404
rescue BusinessLogicError => e
render json: { error: "Business error", code: e.code, details: e.message }, status: 422
rescue ExternalServiceError => e
Rails.logger.error "External service error: #{e.message}"
render json: { error: "Service temporarily unavailable" }, status: 503
rescue => e
Rails.logger.error "Unhandled exception: #{e.class} - #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render json: { error: "Internal server error" }, status: 500
end
end
private
def process_request
# Actual request processing logic
end
end
# Database operation exception handling
class DatabaseManager
def self.with_transaction
ActiveRecord::Base.transaction do
yield
end
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error "Record validation failed: #{e.message}"
raise ValidationError.new("database", nil, "validation", e.message)
rescue ActiveRecord::RecordNotFound => e
Rails.logger.error "Record not found: #{e.message}"
raise RecordNotFound.new(e.message)
rescue ActiveRecord::StatementInvalid => e
Rails.logger.error "SQL statement error: #{e.message}"
raise BusinessLogicError.new("DB001", "Database operation failed")
end
end📚 Next Steps
After mastering Ruby exception handling, continue learning:
- Ruby Object-Oriented Programming - Deep dive into OOP
- Ruby Database Access - Learn database operations
- Ruby Network Programming - Learn Socket programming
- Ruby Multithreading - Master concurrent programming
Continue your Ruby learning journey!