Exception Handling
Ruby uses exceptions to handle errors. When something goes wrong, Ruby raises an exception object. You can catch these exceptions and handle them gracefully instead of letting your program crash. The begin/rescue/ensure/end structure is your safety net.
begin
result = 10 / 0
rescue ZeroDivisionError => e
puts "Error: #{e.message}"
ensure
puts "This runs no matter what"
end
# => Error: divided by 0
# => This runs no matter what
Try it Yourself ->
Exception Class Hierarchy
Ruby has a hierarchy of exception classes. Exception is the base class. StandardError is the main class you'll rescue from. RuntimeError is raised by raise. Knowing the hierarchy helps you catch the right exceptions.
# The hierarchy:
# Exception
# โโโ StandardError
# โ โโโ RuntimeError
# โ โโโ TypeError
# โ โโโ ArgumentError
# โ โโโ NameError
# โ โ โโโ NoMethodError
# โ โโโ IOError
# โ โโโ Errno::* (system errors)
# โ โโโ ZeroDivisionError
# โโโ SignalException
# โ โโโ Interrupt
# โโโ SystemExit
# Always rescue StandardError, not Exception
begin
risky_operation
rescue StandardError => e
puts "Caught: #{e.class} โ #{e.message}"
end
Try it Yourself ->
Custom Exceptions
You can create your own exception classes by inheriting from StandardError. This lets you raise and rescue domain-specific errors in your application.
class InsufficientFundsError < StandardError
attr_reader :balance, :amount
def initialize(balance, amount)
@balance = balance
@amount = amount
super("Cannot withdraw $#{amount} โ balance is $#{balance}")
end
end
class BankAccount
def initialize(balance)
@balance = balance
end
def withdraw(amount)
if amount > @balance
raise InsufficientFundsError.new(@balance, amount)
end
@balance -= amount
end
end
account = BankAccount.new(100)
begin
account.withdraw(150)
rescue InsufficientFundsError => e
puts e.message
puts "Tried: $#{e.amount}, Available: $#{e.balance}"
end
# => Cannot withdraw $150 โ balance is $100
# => Tried: $150, Available: $100
Try it Yourself ->
raise and retry
raise creates and throws an exception. retry runs the rescue block from the beginning. Use retry carefully โ make sure the problem is actually fixable or you'll loop forever.
attempts = 0
begin
attempts += 1
puts "Attempt #{attempts}..."
raise "Connection failed" if attempts < 3
puts "Connected!"
rescue RuntimeError => e
puts "Error: #{e.message}"
retry if attempts < 3
puts "Giving up after #{attempts} attempts"
end
# => Attempt 1...
# => Error: Connection failed
# => Attempt 2...
# => Error: Connection failed
# => Attempt 3...
# => Connected!
Try it Yourself ->
catch/throw for Flow Control
catch and throw aren't for error handling โ they're for non-local exits. They let you break out of deeply nested code instantly. Think of them as labeled gotos that are safe and structured.
catch(:done) do
puts "Starting process..."
10.times do |i|
if i == 5
puts "Found the target at #{i}"
throw(:done, "Success!")
end
puts "Processing #{i}"
end
puts "This never runs"
end
# => Starting process...
# => Processing 0
# => Processing 1
# => Processing 2
# => Processing 3
# => Processing 4
# => Found the target at 5
# Useful for breaking out of nested loops
def find_in_matrix(matrix, target)
catch(:found) do
matrix.each_with_index do |row, i|
row.each_with_index do |val, j|
throw(:found, [i, j]) if val == target
end
end
nil # not found
end
end
Try it Yourself ->