Exceptions

Ruby · Practical · 12 min

Correct error handling separates scripts that silently corrupt data from programs you can trust.

Signal an error with raise ArgumentError, "bad id" (or raise CustomError.new(...)). Handle it with begin/rescue/else/ensure/end:

  • rescue SomeError => e handles that class and its subclasses; list several classes or several clauses, most specific first.
  • else runs when no exception occurred.
  • ensure always runs, which makes it the place for cleanup.
  • retry inside rescue restarts the begin block; always cap attempts.

Method bodies and do ... end blocks act as an implicit begin, so you can put rescue and ensure directly in a def.

The hierarchy matters. A bare rescue catches StandardError and its subclasses: ArgumentError, TypeError, NameError, NoMethodError, ZeroDivisionError, KeyError, IOError and nearly everything an application raises. It deliberately does not catch Exception subclasses like Interrupt (Ctrl-C), SystemExit or NoMemoryError. Never write rescue Exception except at the very top of a process.

Define your own errors by subclassing StandardError, usually under one base class per library (MyGem::Error), so callers can rescue all of them at once. Wrapping a low-level error inside rescue automatically sets the new error's cause, preserving the original.

The inline modifier value = Integer(s) rescue 0 exists but hides all StandardErrors; prefer explicit handling.

Example

class ShopError < StandardError; end
class OutOfStock < ShopError
  def initialize(item) = super("#{item} is out of stock")
end

def buy(item, stock)
  raise OutOfStock, item if stock.fetch(item).zero?
  stock[item] -= 1
end

stock = { tea: 1, cake: 0 }
%i[tea cake pie].each do |item|
  buy(item, stock)
  puts "bought #{item}"           # bought tea
rescue ShopError => e
  puts "shop: #{e.message}"       # shop: cake is out of stock
rescue KeyError
  puts "unknown item #{item}"     # unknown item pie
end
def with_log(name)
  yield
ensure
  puts "closed #{name}"           # runs even when the block raises
end
with_log("db") { Integer("42") }   # closed db

Remember

  • rescue without a class catches StandardError, not Exception
  • ensure always runs; use it for cleanup
  • Give each library one Error base class subclassing StandardError

Watch out for

  • Writing rescue Exception, which also traps Ctrl-C and exit.

Check yourself

  1. Which exception does a bare rescue NOT catch?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.ruby-lang.org/en/3.4/syntax/exceptions_rdoc.html