begin, rescue, and ensure
Fast recall
Quick revision
Core answer
begin to mark code that may fail, rescue to handle named exception classes, and ensure for cleanup that must run whether the work succeeds or raises. Ruby checks matching rescue clauses from top to bottom; if none matches, the exception continues to the caller. A rescue with no class handles StandardError descendants, so production code should usually name the narrow errors it can actually recover from.Whether you know begin wraps, rescue catches (StandardError by default), ensure always runs (cleanup), and to rescue specific classes.
Bare rescue (catches everything, hides bugs) or rescuing Exception (catches SystemExit, breaks signal handling).
begin wraps; rescue catches a class (StandardError default); ensure always runs (cleanup). Rescue specific classes, not bare rescue or Exception.
Interview-ready guide
Interview answer
Ruby runs the statements inside begin in normal order. If one raises, Ruby stops that path immediately, skips the remaining statements in the protected body, and searches for a matching rescue. If no local clause matches, the exception continues up the call stack, while any ensure clauses encountered during that exit still run.
A clause such as rescue JSON::ParserError => error handles that class and its subclasses and exposes the exception object for context. Ruby checks rescue clauses from top to bottom, so specific errors come before a broader parent. A rescue with no class handles StandardError, not every exception; use it only at a boundary that can take responsibility for an ordinary application failure.
ensure runs whether the protected work succeeds, a rescue returns a fallback, or an unhandled exception continues outward. It belongs to cleanup such as closing a resource acquired before the failure. Ruby block APIs like File.open { ... } are often simpler because they already own that cleanup, but the same control-flow rule applies.
A handler should return a documented fallback, translate the error for its caller, or add context and re-raise. Logging and continuing without valid state only delays the failure. An optional else can hold success-only work so the rescue does not accidentally catch errors from that later step. Avoid rescue Exception, which can swallow interrupts, exits, and serious runtime conditions.
begin, rescue, and ensure
Direct Answer
Ruby uses begin to mark code that may fail, rescue to handle named exception classes, and ensure for cleanup that must run whether the work succeeds or raises. Ruby checks matching rescue clauses from top to bottom; if none matches, the exception continues to the caller. A rescue with no class handles StandardError descendants, so production code should usually name the narrow errors it can actually recover from.
Deep Dive & Architecture
An exception changes control flow immediately. Ruby stops the current expression, unwinds method calls, and searches outward for a matching handler. Statements after the failing line do not run. A handler in the same begin block gets the first opportunity; otherwise the caller's handler may receive the exception.
Matching is based on the exception class hierarchy. rescue Errno::ENOENT also handles subclasses of that class. With several clauses, Ruby selects the first match, which is why a broad rescue StandardError placed first would make later specific clauses unreachable in practice.
An optional else clause runs only when the protected body finishes without an exception. It can keep success-only work out of begin, reducing the chance that the rescue accidentally catches an error raised by that later work. ensure is different: it runs for both outcomes and then Ruby continues with the chosen return value or propagating exception.
Handling an exception means taking responsibility for the resulting state. A handler may translate a low-level failure, return a documented fallback, or record context and re-raise. Logging and continuing without a valid result often creates a second, harder-to-diagnose failure later.
Code Example
def load_port(path)
file = nil
begin
file = File.open(path)
Integer(file.read.strip)
rescue Errno::ENOENT
3000 # documented fallback
rescue ArgumentError => error
warn "invalid port in #{path}: #{error.message}"
raise # preserve the original failure
ensure
file&.close # runs on every path
end
end
Common Mistakes & Pitfalls
Bare rescue (catches everything, hides bugs) or rescuing Exception (catches SystemExit, breaks signal handling).
Check your understanding
Answer these without looking back at the article.
- What does a bare `rescue` catch in Ruby?
- When is `ensure` useful, and when is a block-based resource API simpler?
- How does Ruby choose between multiple rescue clauses?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.