Error Handling
## Learning Objectives
- Handle exceptions with begin-rescue
- Use ensure for cleanup
- Raise custom exceptions
- Create custom exception classes
## Exception Hierarchy
```text
Exception
├── StandardError
│ ├── ArgumentError
│ ├── NameError
│ ├── NoMethodError
│ ├── TypeError
│ ├── RuntimeError
│ └── ...
└── SystemExit
```
## Basic Exception Handling
### begin-rescue
```ruby
begin
result = 10 / 0
rescue
puts "An error occurred"
end
```
### With Error Variable
```ruby
begin
result = 10 / 0
rescue => e
puts "Error: #{e.class} - #{e.message}"
end
```
### Multiple rescue Clauses
```ruby
begin
# Code that might fail
result = risky_operation
rescue ArgumentError => e
puts "Invalid argument: #{e.message}"
rescue NoMethodError => e
puts "Method not found: #{e.message}"
rescue => e
puts "Unexpected error: #{e.message}"
end
```
### rescue as Modifier
```ruby
result = risky_operation rescue default_value
```
## Ensure
`ensure` runs whether an exception occurs or not:
```ruby
begin
file = File.open("example.txt", "r")
content = file.read
rescue Errno::ENOENT => e
puts "File not found: #{e.message}"
ensure
file.close if file
end
```
## Retry
`retry` re-executes the begin block:
```ruby
attempts = 0
begin
attempts += 1
connect_to_service
rescue ConnectionError
if attempts < 3
puts "Retrying... (attempt #{attempts})"
retry
else
puts "Failed after #{attempts} attempts"
end
end
```
## Raise
### Raising Standard Errors
```ruby
def divide(a, b)
raise ArgumentError, "Divisor cannot be zero" if b == 0
a / b
end
```
### Raising Custom Exceptions
```ruby
def withdraw(amount)
raise "Insufficient funds" if amount > @balance
@balance -= amount
end
# Better: use specific exception class
class InsufficientFundsError < StandardError
def initialize(balance, amount)
super("Cannot withdraw #{amount}, balance is #{balance}")
end
end
def withdraw(amount)
raise InsufficientFundsError.new(@balance, amount) if amount > @balance
@balance -= amount
end
```
## Custom Exception Classes
```ruby
class ValidationError < StandardError
attr_reader :field
def initialize(message, field = nil)
super(message)
@field = field
end
end
def validate_age(age)
unless age.is_a?(Integer)
raise ValidationError.new("Age must be an integer", :age)
end
unless age >= 0
raise ValidationError.new("Age cannot be negative", :age)
end
end
```
## Catch and Throw
Not exceptions - used for flow control:
```ruby
result = catch(:found) do
[1, 2, 3, 4, 5].each do |n|
throw(:found, n * 2) if n == 3
end
nil
end
puts result # 6
```
## Exception Methods
```ruby
begin
10 / 0
rescue => e
puts e.class # ZeroDivisionError
puts e.message # divided by 0
puts e.backtrace # Array of location strings
puts e.backtrace.first # First line of trace
end
```
## Best Practices
### Do Not Suppress Exceptions
```ruby
# Bad
begin
do_something
rescue
# Do nothing - silently ignore
end
# Good
begin
do_something
rescue SomeError => e
handle_error(e)
end
```
### Raise Early, Catch Late
```ruby
# Raise exceptions early (fail fast)
def process_user(data)
raise ArgumentError, "User data required" if data.nil?
# ... rest of processing
end
# Catch exceptions at appropriate level
def import_users
users.each do |user_data|
begin
process_user(user_data)
rescue ValidationError => e
log_warning("Skipping invalid user: #{e.message}")
end
end
end
```
### Use Specific Exceptions
```ruby
# Bad - too generic
begin
File.open(data_file)
rescue
# Handle error
end
# Good - specific exception
begin
File.open(data_file)
rescue Errno::ENOENT
puts "Data file not found"
rescue Errno::EACCES
puts "Permission denied"
end
```
## Exception Handling Patterns
### nil Pattern
```ruby
def find_user(id)
begin
User.find(id)
rescue ActiveRecord::RecordNotFound
nil
end
end
# Or with rescue modifier
def find_user(id)
User.find(id) rescue nil
end
```
### Default Value Pattern
```ruby
config = begin
YAML.load_file("config.yml")
rescue Errno::ENOENT
{}
end
```
### Logging Pattern
```ruby
require 'logger'
logger = Logger.new(STDOUT)
begin
risky_operation
rescue => e
logger.error("#{e.class}: #{e.message}")
logger.debug(e.backtrace.join("\n"))
raise # Re-raise after logging
end
```
## Summary
- `begin...rescue...ensure` handles exceptions
- `rescue ErrorType => e` captures specific errors
- `raise` throws an exception
- `ensure` runs cleanup code always
- `retry` re-executes begin block
- `catch/throw` for non-exception flow control
- Create custom exceptions by subclassing `StandardError`
- Always handle specific exceptions, not all exceptions
- Use `ensure` for cleanup (file close, etc.)
Comments
Comments powered by Giscus
To enable comments, add your Giscus embed code here.
Learn more about Giscus →