Exceptions and Error Handling

22.1 begin / rescue / else / ensure / retry

def fetch_data(url)
  attempts = 0
  begin
    attempts += 1
    response = HTTPClient.get(url)
  rescue Timeout::Error, Errno::ECONNREFUSED => e
    retry if attempts < 3              # re-run the begin block
    warn "Failed after #{attempts} attempts: #{e.message}"
    nil
  rescue JSON::ParserError => e
    raise DataError, "Invalid JSON from #{url}"   # translates and chains (e.cause is set automatically)
  else
    response                           # runs only if no exception was raised
  ensure
    log("fetch attempted: #{url}")     # always runs
  end
end

def safe_divide(a, b)                  # method-level rescue (no begin needed)
  a / b
rescue ZeroDivisionError
  Float::INFINITY
end

value = Integer(input) rescue 0        # modifier rescue (use sparingly)

22.2 Exception Hierarchy

A bare rescue catches StandardError. Never rescue Exception broadly (it catches Ctrl+C/Interrupt and SystemExit).

Exception
├── NoMemoryError, ScriptError (LoadError, NotImplementedError, SyntaxError)
├── SecurityError, SignalException (Interrupt), SystemExit, SystemStackError
└── StandardError   ← rescue catches this by default
    ├── ArgumentError (UncaughtThrowError), EncodingError, FiberError
    ├── IOError (EOFError), IndexError (KeyError, StopIteration → ClosedQueueError)
    ├── LocalJumpError, NameError (NoMethodError), RangeError (FloatDomainError)
    ├── RegexpError, RuntimeError (FrozenError), ThreadError, TypeError, ZeroDivisionError
    └── SystemCallError (Errno::ENOENT, Errno::EACCES, ...)

22.3 Raising and Custom Exceptions

raise "Something went wrong"                         # RuntimeError
raise ArgumentError, "age must be positive"
raise ArgumentError.new("bad")

module MyApp
  class Error < StandardError; end                   # base error for your library
  class NotFoundError < Error
    attr_reader :resource
    def initialize(resource, msg = "#{resource} not found")
      @resource = resource
      super(msg)
    end
  end
  class ValidationError < Error
    def initialize(errors) = (@errors = errors; super("Validation failed: #{errors.join(', ')}"))
    attr_reader :errors
  end
end

begin
  raise MyApp::NotFoundError.new("User 42")
rescue MyApp::Error => e
  puts "#{e.class}: #{e.message}"
  puts e.backtrace.first(3)
  puts e.full_message(highlight: false)
end

22.4 Error Handling Practices

Rescue specific exceptions. Keep begin blocks small. Don't use exceptions for normal control flow.

Define a library base error (MyLib::Error). Wrap low-level errors while preserving cause.

ensure for cleanup, or better, block-based resource methods (File.open(...) { }).

Use Kernel#exit / abort("msg") in CLIs. Use at_exit hooks. Use Timeout.timeout cautiously (prefer native timeouts in client libraries).

Result objects are an alternative for expected failures (the dry-monads gem: Success/Failure).