Ruby Exceptions

Handling errors gracefully in your Ruby programs

⚠️ What are Exceptions?

Exceptions are errors that occur during program execution. Ruby provides a robust exception handling mechanism to catch and manage errors, preventing your program from crashing unexpectedly.


# Basic exception handling
begin
  result = 10 / 0
rescue ZeroDivisionError
  puts "Cannot divide by zero!"
end
                                    

Output:

Cannot divide by zero!

Key Exception Concepts

🛡️

begin-rescue

Catch and handle exceptions

begin
  # risky code
rescue
  # handle error
end
🎯

Specific Errors

Catch specific exception types

rescue TypeError
  puts "Type error!"
end
🔄

ensure

Code that always runs

ensure
  # cleanup code
  file.close
end
🚀

raise

Throw custom exceptions

raise "Error!"
raise TypeError
end

🔹 Basic Exception Handling

Use begin-rescue blocks to catch exceptions and prevent your program from crashing. The code in the begin block is executed, and if an error occurs, the rescue block handles it gracefully.

begin
  puts "Enter a number:"
  num = gets.chomp.to_i
  result = 100 / num
  puts "Result: #{result}"
rescue ZeroDivisionError
  puts "Error: Cannot divide by zero!"
rescue => e
  puts "An error occurred: #{e.message}"
end

puts "Program continues..."

Output (if user enters 0):

Enter a number:
Error: Cannot divide by zero!
Program continues...

🔹 Multiple Rescue Clauses

You can handle different types of exceptions separately by using multiple rescue clauses. This allows you to provide specific error messages and recovery actions for different error scenarios.

def process_data(data)
  begin
    number = Integer(data)
    result = 100 / number
    puts "Result: #{result}"
  rescue ZeroDivisionError
    puts "Error: Division by zero is not allowed"
  rescue ArgumentError
    puts "Error: Invalid number format"
  rescue TypeError
    puts "Error: Wrong data type provided"
  end
end

process_data("0")      # Division by zero
process_data("abc")    # Invalid format
process_data(nil)      # Wrong type

Output:

Error: Division by zero is not allowed
Error: Invalid number format
Error: Wrong data type provided

🔹 Using ensure

The ensure block always executes, whether an exception occurs or not. This is perfect for cleanup operations like closing files, database connections, or releasing resources that must happen regardless of errors.

def read_file(filename)
  begin
    file = File.open(filename, "r")
    content = file.read
    puts "File content: #{content}"
  rescue Errno::ENOENT
    puts "Error: File not found!"
  rescue => e
    puts "Error reading file: #{e.message}"
  ensure
    file.close if file
    puts "File operation completed"
  end
end

read_file("test.txt")

Output:

Error: File not found!
File operation completed

🔹 Raising Exceptions

You can manually raise exceptions using the raise keyword. This is useful for validating input, enforcing business rules, or signaling error conditions in your code that should be handled by calling code.

def check_age(age)
  raise ArgumentError, "Age cannot be negative" if age < 0
  raise ArgumentError, "Age must be a number" unless age.is_a?(Integer)
  
  if age < 18
    puts "You are a minor"
  else
    puts "You are an adult"
  end
end

begin
  check_age(-5)
rescue ArgumentError => e
  puts "Invalid age: #{e.message}"
end

begin
  check_age(25)
rescue ArgumentError => e
  puts "Invalid age: #{e.message}"
end

Output:

Invalid age: Age cannot be negative
You are an adult

🔹 Custom Exception Classes

Create your own exception classes by inheriting from StandardError. Custom exceptions make your code more readable and allow you to handle specific application errors differently from system errors.

class InsufficientFundsError < StandardError
  def initialize(amount, balance)
    super("Insufficient funds: tried to withdraw #{amount}, but balance is #{balance}")
  end
end

class BankAccount
  attr_reader :balance
  
  def initialize(balance)
    @balance = balance
  end
  
  def withdraw(amount)
    raise InsufficientFundsError.new(amount, @balance) if amount > @balance
    @balance -= amount
    puts "Withdrew #{amount}. New balance: #{@balance}"
  end
end

account = BankAccount.new(100)

begin
  account.withdraw(50)
  account.withdraw(80)
rescue InsufficientFundsError => e
  puts "Transaction failed: #{e.message}"
end

Output:

Withdrew 50. New balance: 50
Transaction failed: Insufficient funds: tried to withdraw 80, but balance is 50

🧠 Test Your Knowledge

Which block always executes regardless of exceptions?