Exceptions
Ruby exceptions are raised and rescued. Build a hierarchy by subclassing StandardError; rescue specific classes, not Exception. ensure for cleanup; retry within rescue.
Raise, rescue, custom, retry, ensure
EXAMPLE
# 1) Define your own hierarchy — subclass StandardError
module Payment
class Error < StandardError; end
class CardDeclined < Error; end
class GatewayDown < Error; end
class InvalidAmount < Error; end
end
# 2) Raise
raise Payment::CardDeclined, "Card xxx-1111 declined"
# Or with attributes
class Payment::CardDeclined < Payment::Error
attr_reader :code
def initialize(message, code:)
super(message)
@code = code
end
end
raise Payment::CardDeclined.new("declined", code: 'INSUFFICIENT_FUNDS')
# 3) Rescue — specific first, generic last
begin
process_payment(amount: 100, card: card)
rescue Payment::CardDeclined => e
log.warn("card declined: \#{e.code} #{e.message}")
notify_user(e.message)
rescue Payment::GatewayDown
schedule_retry
rescue Payment::Error => e
log.error(e)
raise # re-raise unknown payment errors
ensure
close_connection # runs even if no exception
end
# 4) NEVER `rescue Exception`
# Exception is the parent of SystemExit, Interrupt (Ctrl+C), SignalException.
# Default `rescue` (no class) catches StandardError — the right behaviour.
# BAD
# begin work; rescue Exception => e; ...; end
# GOOD
begin
work
rescue => e # StandardError + its subclasses
log.error(e)
end
# 5) Retry — for transient failures
attempts = 0
begin
attempts += 1
api.call
rescue Net::OpenTimeout, Net::ReadTimeout => e
retry if attempts < 3
raise
end
# 6) Inline rescue (return on failure) — handy but use sparingly
result = fetch_from_cache rescue nil
# 7) Method-level rescue
def create_user(params)
User.create!(params)
rescue ActiveRecord::RecordInvalid => e
{ error: e.record.errors.full_messages }
end
# 8) Raising with .new vs raising the class
raise Payment::Error # default message = class name
raise Payment::Error, "declined" # message via 2nd arg
raise Payment::Error.new("declined") # equivalent
raise Payment::CardDeclined.new("declined", code: 'X') # custom attrs
# 9) Exception object info
rescue => e
e.class # the exception class
e.message # the message string
e.backtrace # array of "file:line:in `method'" frames
e.cause # the original exception, if one was wrapping another (Ruby 2.1+)
end
# 10) Chain exceptions — explicit cause
begin
do_low_level_work
rescue StandardError => low
raise HighLevelError, "failed at top level"
# The new exception's `cause` is automatically the low-level one.
end
# 11) Common idioms
# Try/return — if all OK, return; on error, return a tuple
def safe_parse(s)
[JSON.parse(s), nil]
rescue JSON::ParserError => e
[nil, e]
end
# Wrap untrusted code with a clean error
def with_retries(times: 3)
attempts = 0
begin
attempts += 1
yield
rescue StandardError => e
retry if attempts < times
raise
end
end
with_retries { http.get(url) }
# 12) Logging — backtrace is gold
rescue => e
log.error("\#{e.class}: \#{e.message}")
log.error(e.backtrace.first(20).join(\"\\n\"))
end
# 13) Test exceptions
# RSpec
expect { do_thing }.to raise_error(Payment::CardDeclined, /declined/)
expect { do_thing }.to raise_error(Payment::CardDeclined) { |e| expect(e.code).to eq('X') }
# 14) When to raise vs return a result
# - Raise for EXCEPTIONAL conditions (DB down, invalid state, programmer error)
# - Return a value (or Result/Either) for expected outcomes (validation, lookup miss)
Why it matters
Build a small exception hierarchy under StandardError and rescue specifically. Bare rescue => e catches StandardError (good); rescue Exception catches SIGINT and breaks Ctrl+C (bad).
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
begin
raise 'boom' if bad?
rescue StandardError => e
puts "caught: #{e.message}"
ensure
cleanup
end
Try it Yourself »
Discussion
Loading…