Ruby Exceptions
Ruby Exception Handling
Section titled “Ruby Exception Handling”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.
Basic begin/rescue Syntax
Section titled “Basic begin/rescue Syntax”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). # ...endHow it Works:
Section titled “How it Works:”- The code within the
begin...rescueblock is executed. - If an exception occurs, Ruby looks for a
rescueclause that matches the type of exception raised (or one of its superclasses). - If a match is found, the code in that
rescueblock is executed. The exception object itself can be captured using=> e(whereeis a variable). - If no specific exception type is given in a
rescueclause (i.e., justrescue), it defaults to rescuingStandardErrorand its descendants. It is best practice to rescue specific exceptions or at leastStandardErrorrather thanException(which can catch signals likeInterruptorSystemExitthat you might not want to interfere with). - The
elseclause (optional) executes only if thebeginblock completes without raising any exceptions. - The
ensureclause (optional) always executes, regardless of whether an exception occurred, was rescued, or if the block exited normally. It’s crucial for cleanup tasks.
Example:
Section titled “Example:”#!/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."endIf you enter non-numeric input or 0 for the second number, the respective rescue block will handle the error.
The retry Statement
Section titled “The retry Statement”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.
Syntax:
Section titled “Syntax:”begin # ... code that might fail ...rescue SomeError => e # ... handle error, perhaps modify state ... retry # Jumps to the start of the 'begin' blockendExample:
Section titled “Example:”#!/usr/bin/env ruby# frozen_string_literal: true
attempts = 0max_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." endrescue StandardError => e puts "Another error occurred: #{e.message}"endThe raise Statement
Section titled “The raise Statement”You can explicitly raise (or throw) an exception using the raise statement. This is useful for signaling error conditions in your own code.
Syntax Forms:
Section titled “Syntax Forms:”# 1. Re-raise the current exception (often used in a rescue block)raise
# 2. Raise a RuntimeError with a specific messageraise "Error Message"
# 3. Raise a specific type of exception with a messageraise 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 conditionExample:
Section titled “Example:”#!/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 StandardErrorrescue 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.Capturing Exception Information
Section titled “Capturing Exception Information”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 rubybegin 1 / 0rescue 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}" }endThe $! 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
Section titled “catch and throw”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.
Syntax:
Section titled “Syntax:”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 happensend
puts result # => "Finished at iteration 5"Example: Terminating User Input Loop
Section titled “Example: Terminating User Input Loop”#!/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' inputend
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 givenend
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."endThe Ruby Exception Hierarchy
Section titled “The Ruby Exception Hierarchy”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 ...Creating Custom Exceptions
Section titled “Creating Custom Exceptions”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 endend
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}" endend
service = ApiService.newurls_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}" endend