Overview
Ruby implements exception handling through begin/rescue/ensure/end
blocks that catch and process exceptions while ensuring cleanup code executes regardless of success or failure. The rescue
clause catches exceptions of specified types, ensure
runs cleanup code unconditionally, and end
terminates the block structure.
Ruby's exception system centers on the Exception
class hierarchy. All rescuable exceptions inherit from StandardError
, while system-level exceptions like SystemExit
and Interrupt
inherit directly from Exception
. The rescue mechanism defaults to catching StandardError
and its subclasses unless explicitly configured otherwise.
The basic structure separates normal execution flow in the begin
section from error handling in rescue
clauses. Multiple rescue
clauses can handle different exception types, with Ruby evaluating them in order until finding a match. The ensure
clause executes after all other processing completes, whether the block exits normally or through an exception.
begin
# Normal execution code
result = risky_operation
rescue StandardError => e
# Exception handling
handle_error(e)
ensure
# Cleanup code
cleanup_resources
end
Ruby provides syntactic sugar for method-level exception handling without explicit begin
blocks. Method definitions can include rescue
and ensure
clauses directly, treating the entire method body as the begin
section.
def process_data
perform_calculations
rescue ArgumentError => e
log_error(e)
nil
ensure
close_connections
end
The retry
keyword restarts execution from the beginning of the begin
block, allowing recovery attempts after handling exceptions. Ruby limits retry attempts to prevent infinite loops, though developers must implement their own retry counting for production use.
Basic Usage
Exception handling requires matching the rescue clause to the expected exception types. Ruby evaluates rescue clauses from top to bottom, executing the first matching clause and skipping subsequent ones. Specific exception types should precede general ones to ensure proper matching.
begin
data = JSON.parse(input_string)
process_data(data)
rescue JSON::ParserError => e
puts "Invalid JSON format: #{e.message}"
return nil
rescue StandardError => e
puts "Processing error: #{e.message}"
return nil
end
The ensure
clause executes regardless of how the block terminates. This guarantees resource cleanup even when exceptions occur or early returns execute. File handles, database connections, and network sockets commonly require ensure-based cleanup.
begin
file = File.open('data.txt', 'r')
content = file.read
process_content(content)
rescue IOError => e
puts "File access error: #{e.message}"
ensure
file&.close
end
Rescue clauses can capture exceptions in variables using the => variable
syntax. The exception object provides access to the message, backtrace, and other diagnostic information. Ruby assigns the caught exception to the specified variable for inspection within the rescue block.
begin
unsafe_operation
rescue RuntimeError => error
puts "Error occurred: #{error.message}"
puts "Backtrace: #{error.backtrace.join("\n")}"
end
Multiple exception types can be handled by a single rescue clause using array syntax. This approach reduces code duplication when identical handling applies to several exception types.
begin
network_request
rescue [Timeout::Error, Net::ReadTimeout, Net::OpenTimeout] => e
puts "Network timeout: #{e.message}"
retry_with_backoff
end
The else
clause executes only when no exceptions occur in the begin block. This provides a clean separation between normal continuation logic and exception handling paths.
begin
result = calculate_value
rescue ZeroDivisionError
puts "Division by zero detected"
result = 0
else
puts "Calculation successful: #{result}"
ensure
cleanup_calculations
end
Error Handling & Debugging
Exception objects carry extensive diagnostic information beyond the basic message. The backtrace
method returns an array of strings showing the call stack at the exception point, while cause
provides access to wrapped or chained exceptions that triggered the current error.
begin
nested_operation
rescue StandardError => e
puts "Primary error: #{e.message}"
puts "Error class: #{e.class}"
puts "Backtrace:"
e.backtrace.each_with_index do |line, index|
puts " #{index}: #{line}"
end
if e.cause
puts "Underlying cause: #{e.cause.message}"
end
end
Custom exception classes enable domain-specific error handling and provide structured information about application-specific failures. Inheriting from StandardError
ensures these exceptions integrate properly with rescue mechanisms.
class ValidationError < StandardError
attr_reader :field, :value, :constraint
def initialize(field, value, constraint)
@field = field
@value = value
@constraint = constraint
super("#{field} value '#{value}' violates constraint: #{constraint}")
end
end
begin
validate_user_input(user_data)
rescue ValidationError => e
puts "Validation failed for #{e.field}"
puts "Invalid value: #{e.value}"
puts "Constraint: #{e.constraint}"
end
Exception filtering based on message content or other attributes provides fine-grained error handling. The rescue clause can include conditional logic to handle specific error scenarios differently.
begin
database_operation
rescue ActiveRecord::RecordInvalid => e
case e.message
when /email.*taken/
handle_duplicate_email
when /password.*too short/
handle_weak_password
else
handle_generic_validation_error(e)
end
end
Logging strategies for exceptions should capture sufficient context for debugging while avoiding sensitive information exposure. Structured logging with consistent exception formatting aids troubleshooting across application components.
def log_exception(exception, context = {})
error_data = {
error_class: exception.class.name,
error_message: exception.message,
backtrace: exception.backtrace,
context: context,
timestamp: Time.now.utc.iso8601
}
logger.error(error_data.to_json)
end
begin
process_payment(amount, card)
rescue PaymentError => e
log_exception(e, { amount: amount, card_type: card.type })
raise
end
Advanced Usage
Exception handling blocks can be nested to create layered error recovery strategies. Inner rescue clauses handle specific failures while outer clauses provide fallback handling for broader categories of errors.
begin
begin
primary_service_call
rescue ServiceUnavailableError
fallback_service_call
end
rescue NetworkError => e
puts "All network operations failed: #{e.message}"
use_cached_data
rescue StandardError => e
puts "Unexpected error: #{e.message}"
safe_degraded_mode
end
The retry
mechanism supports sophisticated recovery patterns with exponential backoff and maximum attempt limits. Implementing retry counters and delay strategies prevents overwhelming failing services while providing reasonable recovery opportunities.
def resilient_api_call(max_retries: 3, base_delay: 1)
retries = 0
begin
make_api_request
rescue Net::TimeoutError, Net::ReadTimeout => e
retries += 1
if retries <= max_retries
delay = base_delay * (2 ** (retries - 1))
puts "Request failed, retrying in #{delay} seconds (attempt #{retries})"
sleep(delay)
retry
else
puts "Max retries exceeded, giving up"
raise
end
end
end
Exception handling integrates with Ruby's block and iterator patterns. Methods can rescue exceptions from yielded blocks while maintaining clean separation between the calling context and block execution.
def with_error_handling(operation_name)
begin
yield
rescue StandardError => e
puts "Error in #{operation_name}: #{e.message}"
log_exception(e, operation: operation_name)
nil
end
end
result = with_error_handling("data processing") do
complex_data_transformation(input)
end
Module-based exception handling provides reusable error management strategies across multiple classes. Mixing in exception handling modules creates consistent error behavior while maintaining code organization.
module ExceptionLogging
private
def with_exception_logging(operation)
begin
yield
rescue StandardError => e
self.class.logger.error(
"#{self.class.name}##{operation} failed: #{e.message}"
)
raise
end
end
end
class DataProcessor
include ExceptionLogging
def process_batch(items)
with_exception_logging(__method__) do
items.map { |item| transform_item(item) }
end
end
end
Common Pitfalls
Rescuing Exception
instead of StandardError
catches system-level exceptions like SystemExit
and Interrupt
, preventing proper program termination. This interferes with process signals and can make applications difficult to stop cleanly.
# Problematic - catches system exceptions
begin
long_running_operation
rescue Exception => e # Don't do this
puts "Error: #{e.message}"
end
# Correct - only catches application exceptions
begin
long_running_operation
rescue StandardError => e
puts "Error: #{e.message}"
end
Empty rescue clauses that swallow exceptions without logging or handling create silent failures that are extremely difficult to debug. Always log caught exceptions or provide alternative handling logic.
# Problematic - silent failure
begin
risky_operation
rescue
# Silent failure - debugging nightmare
end
# Better - log the exception
begin
risky_operation
rescue StandardError => e
logger.error("Operation failed: #{e.message}")
logger.debug(e.backtrace.join("\n"))
nil
end
Rescue clauses that are too broad can mask unrelated errors and create confusing debugging scenarios. Specific exception types provide clearer error handling and prevent catching unexpected failures.
# Problematic - too broad
begin
user = User.find(id)
user.update_attributes(params)
rescue StandardError
render json: { error: "Something went wrong" }
end
# Better - specific exception handling
begin
user = User.find(id)
user.update_attributes!(params)
rescue ActiveRecord::RecordNotFound
render json: { error: "User not found" }, status: 404
rescue ActiveRecord::RecordInvalid => e
render json: { error: e.message }, status: 422
end
Infinite retry loops occur when retry conditions never change between attempts. Always implement retry limits and ensure the conditions causing failures can potentially resolve.
# Problematic - potential infinite loop
begin
connect_to_database
rescue ConnectionError
retry # Will retry forever if database is down
end
# Better - limited retries with backoff
retries = 0
begin
connect_to_database
rescue ConnectionError => e
retries += 1
if retries < 5
sleep(retries * 2)
retry
else
raise e
end
end
Resource leaks occur when ensure clauses don't properly handle partially initialized resources. Check for nil values and use safe navigation when cleaning up resources that might not have been fully created.
# Problematic - potential nil reference
begin
connection = establish_connection
file = File.open(filename)
process_data(connection, file)
ensure
connection.close # Fails if connection was never established
file.close # Fails if file was never opened
end
# Better - safe cleanup
begin
connection = establish_connection
file = File.open(filename)
process_data(connection, file)
ensure
connection&.close
file&.close
end
Reference
Exception Hierarchy
Exception Class | Parent | Purpose |
---|---|---|
Exception |
Object | Root exception class |
SystemExit |
Exception | Process termination |
Interrupt |
Exception | User interruption (Ctrl+C) |
StandardError |
Exception | Application exceptions |
RuntimeError |
StandardError | Generic runtime failures |
ArgumentError |
StandardError | Invalid argument values |
TypeError |
StandardError | Type-related errors |
NameError |
StandardError | Undefined names/constants |
NoMethodError |
NameError | Undefined method calls |
IOError |
StandardError | Input/output failures |
SystemCallError |
StandardError | System call failures |
Syntax Reference
Structure | Syntax | Description |
---|---|---|
Basic rescue | begin; code; rescue; handler; end |
Catches StandardError |
Typed rescue | begin; code; rescue Type => e; handler; end |
Catches specific type |
Multiple types | begin; code; rescue Type1, Type2 => e; handler; end |
Catches multiple types |
Multiple clauses | begin; code; rescue Type1; handle1; rescue Type2; handle2; end |
Sequential type matching |
With ensure | begin; code; rescue; handler; ensure; cleanup; end |
Guaranteed cleanup |
With else | begin; code; rescue; handler; else; success; end |
Success-only execution |
Method rescue | def method; code; rescue; handler; end |
Method-level handling |
Control Flow Keywords
Keyword | Context | Effect |
---|---|---|
retry |
rescue clause | Restarts begin block execution |
raise |
anywhere | Throws exception (re-raise in rescue) |
return |
rescue/ensure | Exits method with value |
break |
rescue/ensure in block | Exits block iteration |
next |
rescue/ensure in block | Continues to next iteration |
Exception Object Methods
Method | Returns | Description |
---|---|---|
#message |
String | Exception message text |
#backtrace |
Array<String> | Call stack trace |
#backtrace_locations |
Array<Thread::Backtrace::Location> | Structured backtrace |
#cause |
Exception or nil | Wrapped/chained exception |
#class |
Class | Exception class object |
#inspect |
String | Debug representation |
#set_backtrace(bt) |
Array | Sets custom backtrace |
Raise Method Signatures
Signature | Effect |
---|---|
raise |
Re-raises current exception |
raise(message) |
Raises RuntimeError with message |
raise(ExceptionClass) |
Raises exception of specified class |
raise(ExceptionClass, message) |
Raises exception with custom message |
raise(exception_object) |
Raises pre-constructed exception |
Common Exception Types
Exception | Triggered By | Typical Handling |
---|---|---|
ArgumentError |
Invalid method arguments | Validate inputs, provide defaults |
TypeError |
Wrong object types | Type checking, conversion |
NoMethodError |
Undefined method calls | Check object responds, provide alternatives |
NameError |
Undefined variables/constants | Check existence, lazy loading |
IOError |
File/stream operations | Retry, alternative sources |
JSON::ParserError |
Invalid JSON parsing | Validate format, fallback data |
Net::TimeoutError |
Network timeouts | Retry with backoff, cached responses |
ActiveRecord::RecordNotFound |
Missing database records | Handle missing data gracefully |