Skip to content

Ruby Exceptions

Exceptions are Ruby’s way of dealing with errors or unexpected events that occur during program execution. If not handled, an exception will typically cause the program to terminate. Proper exception handling allows a program to respond gracefully to errors, log issues, retry operations, or take alternative actions.

Ruby uses begin, rescue, else, ensure, and end keywords to manage exceptions.

begin
# Code that might raise an exception (the "protected" block)
# ...
rescue StandardError => variable_for_exception_object
# Code to handle the specific StandardError or its subclasses
# 'variable_for_exception_object' now holds the exception object
# ...
rescue AnotherExceptionType => e
# Code to handle AnotherExceptionType
# ...
rescue
# Code to handle any exception that inherits from StandardError
# (if no specific type is given, defaults to StandardError)
# It's generally better to rescue specific exception types.
else
# Code that executes only if NO exceptions were raised in the 'begin' block
# ...
ensure
# Code that ALWAYS executes, whether an exception occurred or not.
# Often used for cleanup (e.g., closing files, releasing resources).
# ...
end
  • The code within the begin...rescue block is executed.
  • If an exception occurs, Ruby looks for a rescue clause that matches the type of exception raised (or one of its superclasses).
  • If a match is found, the code in that rescue block is executed. The exception object itself can be captured using => e (where e is a variable).
  • If no specific exception type is given in a rescue clause (i.e., just rescue), it defaults to rescuing StandardError and its descendants. It is best practice to rescue specific exceptions or at least StandardError rather than Exception (which can catch signals like Interrupt or SystemExit that you might not want to interfere with).
  • The else clause (optional) executes only if the begin block completes without raising any exceptions.
  • The ensure clause (optional) always executes, regardless of whether an exception occurred, was rescued, or if the block exited normally. It’s crucial for cleanup tasks.
#!/usr/bin/env ruby
# frozen_string_literal: true
begin
print "Enter a number: "
num1_str = gets.chomp
num1 = Integer(num1_str) # This can raise ArgumentError for invalid input
print "Enter another number: "
num2_str = gets.chomp
num2 = Integer(num2_str)
# This can raise ZeroDivisionError if num2 is 0
result = num1 / num2
puts "Result of division: #{result}"
rescue ArgumentError => e
puts "Invalid input: Please enter valid numbers. Details: #{e.message}"
rescue ZeroDivisionError => e
puts "Error: Cannot divide by zero. Details: #{e.message}"
rescue StandardError => e
puts "An unexpected standard error occurred: #{e.message}"
puts "Backtrace: #{e.backtrace.join("\n")}"
else
puts "Calculation successful, no errors!"
ensure
puts "Exiting calculation block."
end

If you enter non-numeric input or 0 for the second number, the respective rescue block will handle the error.

Inside a rescue block, the retry statement causes Ruby to jump back to the beginning of the begin block and re-execute the code. Use retry with caution to avoid infinite loops, typically by including a counter or changing conditions.

begin
# ... code that might fail ...
rescue SomeError => e
# ... handle error, perhaps modify state ...
retry # Jumps to the start of the 'begin' block
end
#!/usr/bin/env ruby
# frozen_string_literal: true
attempts = 0
max_attempts = 3
begin
attempts += 1
puts "Attempt ##{attempts}"
print "Enter filename to open: "
filename = gets.chomp
file = File.open(filename, "r")
puts "File '#{filename}' opened successfully."
puts file.read(50) # Read first 50 chars
file.close
rescue Errno::ENOENT => e # File Not Found Error
puts "Error: #{e.message}"
if attempts < max_attempts
puts "Retrying..."
retry
else
puts "Maximum attempts reached. Could not open file."
end
rescue StandardError => e
puts "Another error occurred: #{e.message}"
end

You can explicitly raise (or throw) an exception using the raise statement. This is useful for signaling error conditions in your own code.

# 1. Re-raise the current exception (often used in a rescue block)
raise
# 2. Raise a RuntimeError with a specific message
raise "Error Message"
# 3. Raise a specific type of exception with a message
raise ExceptionType, "Error Message"
# 4. Raise a specific exception object (allows setting custom attributes)
custom_error = MyCustomError.new("Detailed message")
# custom_error.details = { code: 123 }
raise custom_error
# 5. Conditional raise (less common form, often `raise if condition` is clearer)
# raise ExceptionType, "Error Message" if condition
#!/usr/bin/env ruby
# frozen_string_literal: true
def check_age(age)
if age < 0
raise ArgumentError, "Age cannot be negative."
elsif age < 18
raise StandardError, "User is underage."
end
puts "Age #{age} is valid."
end
begin
check_age(25)
check_age(-5) # This will raise ArgumentError
# check_age(16) # This would raise StandardError
rescue ArgumentError => e
puts "Caught ArgumentError: #{e.message}"
rescue StandardError => e
puts "Caught StandardError: #{e.message}"
end
puts "After age check block."

This produces:

Age 25 is valid.
Caught ArgumentError: Age cannot be negative.
After age check block.

When an exception is rescued (e.g., rescue => e), the variable e holds the exception object. Common methods on exception objects include:

  • e.message: The error message.
  • e.backtrace: An array of strings representing the call stack at the point of the exception.
  • e.class: The class of the exception.
#!/usr/bin/env ruby
begin
1 / 0
rescue ZeroDivisionError => e
puts "Error Class: #{e.class}"
puts "Error Message: #{e.message}"
puts "Backtrace (first 3 lines):"
e.backtrace.first(3).each { |line| puts " #{line}" }
end

The $! global variable also holds the last exception raised, and $@ holds its backtrace, but using the => e syntax is generally preferred for clarity and scope.

catch and throw provide a mechanism for non-local jumps, used for breaking out of deeply nested constructs during normal processing, not primarily for error handling (which is raise/rescue’s domain).

catch(:label_name) do ... end defines a block. A throw(:label_name) or throw(:label_name, return_value) within that block (or any method called from within it) will immediately jump to the end of the catch block. The catch block then returns the second argument to throw, or nil if none was given.

result = catch(:done) do
# ... some processing ...
10.times do |i|
# ... more processing ...
if i == 5
throw :done, "Finished at iteration #{i}" # Jump out with a return value
end
end
"Completed all iterations" # This won't be reached if throw happens
end
puts result # => "Finished at iteration 5"
#!/usr/bin/env ruby
# frozen_string_literal: true
def process_input(prompt_message)
print prompt_message
input = gets.chomp
throw :quit_requested if input.downcase == 'quit'
input
end
puts "Enter data for each field. Type 'quit' to exit early."
user_data = catch(:quit_requested) do
name = process_input("Name: ")
age = process_input("Age: ")
city = process_input("City: ")
{ name: name, age: age, city: city } # Return hash if all inputs given
end
if user_data.is_a?(Hash)
puts "\nUser data collected:"
user_data.each { |key, value| puts " #{key.capitalize}: #{value}" }
else
puts "\nInput process was quit early."
end

All exception classes in Ruby inherit from the Exception class. However, it’s generally advised to rescue from StandardError or its more specific descendants, as Exception includes system-level events like SystemExit (from exit) and Interrupt (from Ctrl-C) which you usually don’t want to catch.

A simplified view of the hierarchy:

Exception
├── NoMemoryError
├── ScriptError
│ ├── LoadError (e.g., `require` fails)
│ └── SyntaxError
├── SignalException
│ └── Interrupt (Ctrl-C)
├── StandardError (Most common application-level errors inherit from this)
│ ├── ArgumentError
│ ├── IOError
│ │ └── EOFError
│ ├── IndexError
│ ├── NameError
│ │ └── NoMethodError
│ ├── RangeError
│ ├── RegexpError
│ ├── RuntimeError (Default for `raise "message"`)
│ ├── TypeError
│ └── ZeroDivisionError
│ └── ... many others ...
├── SystemExit (from `exit`)
└── ... other internal errors ...

It’s good practice to define custom exception classes for your application or library errors. These should typically inherit from StandardError or one of its descendants.

#!/usr/bin/env ruby
# frozen_string_literal: true
class NetworkDataError < StandardError
attr_reader :error_code
def initialize(message = "Network data processing error", error_code = nil)
super(message)
@error_code = error_code
end
end
class ApiService
def fetch_data(url)
# Simulate an API call that might fail
if url.include?("error_site")
raise NetworkDataError.new("Failed to fetch from #{url}", 503)
elsif url.include?("empty_site")
raise NetworkDataError.new("No data received from #{url}", 204)
end
"Sample data from #{url}"
end
end
service = ApiService.new
urls_to_try = ["http://example.com", "http://error_site.com", "http://empty_site.com"]
urls_to_try.each do |url|
begin
data = service.fetch_data(url)
puts "Success for #{url}: #{data}"
rescue NetworkDataError => e
puts "Custom Error for #{url}: #{e.message} (Code: #{e.error_code || 'N/A'})"
rescue StandardError => e
puts "Generic Error for #{url}: #{e.message}"
end
end