100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Ruby

Exception Handling in Ruby

Understand Ruby's begin/rescue/ensure/raise vocabulary for handling errors gracefully, including exception hierarchies, retry, and best practices for robust code.

Error Handling & MetaprogrammingIntermediate9 min readJul 9, 2026
Analogies

Exception Handling in Ruby

Things go wrong: files are missing, network calls time out, user input is malformed, division happens by zero. Ruby handles these situations through exceptions — objects that represent an error condition and can be raised, caught, and inspected. Ruby's exception-handling vocabulary (begin/rescue/else/ensure/end, along with the raise keyword) is deliberately similar to try/catch/finally in other languages, but with Ruby's own conventions: exceptions are ordinary objects organized in a class hierarchy rooted at Exception, and rescuing the wrong class — or rescuing too broadly — is one of the most common sources of subtle bugs in production Ruby code.

🏏

Cricket analogy: Rain stopping play, a run-out review going wrong, or a bat breaking mid-shot are all things that go wrong on the field; Ruby's begin/rescue/ensure structure is the umpire's protocol for handling disruptions without abandoning the match, but rescuing the wrong class is like stopping play for a non-issue.

begin/rescue/else/ensure

A begin block wraps code that might fail. One or more rescue clauses catch specific exception classes and handle them. An optional else clause runs only if no exception was raised, and ensure always runs, whether or not an exception occurred — making it the natural place for cleanup like closing files or releasing locks.

🏏

Cricket analogy: A begin block is like attempting a risky run between wickets; rescue clauses are the specific recovery plans for a run-out versus a dropped bat, else runs only if the run was completed safely, and ensure always happens — like the physio checking on the batsman regardless.

ruby
def read_config(path)
  file = File.open(path)
  contents = file.read
  JSON.parse(contents)
rescue Errno::ENOENT => e
  warn "Config file missing: #{e.message}"
  {}
rescue JSON::ParserError => e
  warn "Config file is not valid JSON: #{e.message}"
  {}
else
  puts "Config loaded successfully"
ensure
  file&.close
end

Methods (and blocks in modern Ruby) can use rescue/ensure directly without an explicit begin/end wrapper, as shown in the example above — begin is only required when you want to rescue within an arbitrary code block rather than a whole method body.

The exception hierarchy and raise

Ruby's built-in exceptions descend from Exception, with StandardError as the ancestor of most everyday errors (ArgumentError, TypeError, ZeroDivisionError, RuntimeError, IOError, and so on). A bare rescue with no class specified only catches StandardError and its subclasses — it deliberately does not catch things like SystemExit, NoMemoryError, or Interrupt (Ctrl-C), which descend directly from Exception. This is why rescue Exception is almost always wrong: it swallows signals and system-level conditions the program should not silently absorb.

🏏

Cricket analogy: StandardError is like the everyday match incidents (no-balls, wides, dropped catches) a rescue clause is meant to handle, while things like the stadium losing power entirely descend from a more catastrophic category that no ordinary rescue Exception should silently absorb.

ruby
def divide(a, b)
  raise ArgumentError, "divisor cannot be zero" if b.zero?

  a / b
end

begin
  divide(10, 0)
rescue ArgumentError => e
  puts "Bad input: #{e.message}"
end

# raise with no arguments re-raises the current exception inside a rescue block
begin
  begin
    raise "inner failure"
  rescue => e
    puts "logging and re-raising"
    raise
  end
rescue => e
  puts "outer caught: #{e.message}"  #=> outer caught: inner failure
end

rescue => e (no explicit class) is shorthand for rescue StandardError => e, not for rescuing everything. Beginners often assume a bare rescue catches all errors — it does not catch Exception subclasses outside StandardError, which is almost always the behavior you want.

retry and controlling flow

The retry keyword, used inside a rescue clause, jumps back to the beginning of the begin block and runs it again — useful for transient failures like a flaky network call, provided you guard against infinite retry loops with a counter or a maximum attempt limit.

🏏

Cricket analogy: retry is like a bowler who overstepped and bowled a no-ball getting to bowl that delivery again immediately, but a captain still caps how many times a bowler can be recalled for the same over to avoid an endless loop of no-balls.

ruby
attempts = 0

begin
  attempts += 1
  response = fetch_from_flaky_api
rescue Net::OpenTimeout => e
  if attempts < 3
    sleep(attempts) # simple backoff
    retry
  else
    raise "Giving up after 3 attempts: #{e.message}"
  end
end
  • begin/rescue/else/ensure/end handles errors; ensure always runs, else only runs when no exception occurred.
  • Methods can rescue directly without a begin wrapper, but blocks need an explicit begin/end.
  • A bare rescue catches StandardError and subclasses only — never rescue Exception directly.
  • raise with a class and message creates a new exception; bare raise inside a rescue re-raises the current one.
  • retry restarts the begin block, so always pair it with an attempt counter to avoid infinite loops.
  • Custom, specific exception classes should be preferred over generic RuntimeError for anything an API caller needs to distinguish.

Practice what you learned

Was this page helpful?

Topics covered

#Ruby#RubyStudyNotes#Programming#ExceptionHandlingInRuby#Exception#Handling#Begin#Rescue#ErrorHandling#StudyNotes#SkillVeris