Skip to content
AgileLogics
7 min read

Exceptions in Depth

Prerequisites: Basic error handling, Inheritance

The beginner lesson covered rescue. Real programs also need to raise errors, clean up after them, and define their own error types.

raise

def withdraw(balance, amount)
  raise ArgumentError, "amount must be positive" unless amount.positive?

  balance - amount
end

raise ErrorClass, "message" stops the method and sends the error up to whichever caller rescues it.

Custom error classes

Define your own errors by inheriting from StandardError. Callers can then rescue exactly the problems they know how to handle:

class InsufficientFunds < StandardError
  attr_reader :shortfall

  def initialize(shortfall)
    @shortfall = shortfall
    super("insufficient funds: short by #{shortfall}")
  end
end

def withdraw(balance, amount)
  raise InsufficientFunds.new(amount - balance) if amount > balance

  balance - amount
end

begin
  withdraw(50, 80)
rescue InsufficientFunds => e
  puts e.message
  puts "Please deposit #{e.shortfall} more"
end
insufficient funds: short by 30
Please deposit 30 more

Inherit from StandardError, not Exception. A bare rescue only catches StandardError and its subclasses; Exception also covers things like pressing Ctrl + C (Interrupt), which should normally stop the program.

The full structure: rescue, else, ensure

def process(value)
  puts "start"
  result = 100 / value
rescue ZeroDivisionError => e
  puts "rescued: #{e.message}"
else
  puts "no error, result #{result}"
ensure
  puts "cleanup always runs"
end

process(4)
process(0)
start
no error, result 25
cleanup always runs
start
rescued: divided by 0
cleanup always runs
  • else runs only if nothing was raised.
  • ensure runs no matter what — use it for cleanup such as closing a file or a connection.

retry

retry re-runs the begin block — useful for flaky operations such as network calls. Always limit the attempts:

attempts = 0

begin
  attempts += 1
  raise IOError, "network down" if attempts < 3

  puts "succeeded on attempt #{attempts}"
rescue IOError
  retry if attempts < 3
  puts "giving up"
end
succeeded on attempt 3

Common mistakes

  • rescue Exception — it swallows Ctrl + C and system errors. Rescue StandardError or something more specific.
  • Rescuing and doing nothing (rescue; end). The error vanishes, and you lose the information needed to fix the bug. At minimum, log it.
  • retry with no limit — an endless loop if the problem never clears.

Practice

Define an InvalidAge error. Write set_age(value) that raises it with a helpful message when the value isn't between 0 and 150, and rescue it when calling set_age(200).

Show solution
class InvalidAge < StandardError; end

def set_age(value)
  raise InvalidAge, "age #{value} is out of range (0-150)" unless (0..150).cover?(value)

  value
end

begin
  set_age(200)
rescue InvalidAge => e
  puts "Error: #{e.message}"
end
Error: age 200 is out of range (0-150)