skip to content

In Ruby, how does a begin block choose among several rescue clauses, and why must specific classes come before broader ones?

level: middleimportance: should knowfreq 45%

answer

  1. top to bottom, first match wins
  2. class === exception decides
  3. comma lists and *splatted arrays
  4. non-class: TypeError when matched
  5. Lint/ShadowedException

basics

~20 s

Ruby tests rescue clauses top to bottom and runs only the first whose listed class or module matches the exception via ===. A broad class listed first, like StandardError above ArgumentError, catches everything, so the narrower clause never runs.

solid answer

~40 s

When the body raises, Ruby walks the `rescue` clauses in source order and, for each class or module listed, calls `klass === exception`, which for classes is an `is_a?` test. The first clause with a match runs and the rest are ignored, so `rescue StandardError` above `rescue ArgumentError` shadows it completely; RuboCop's `Lint/ShadowedException` reports that. One clause may list several classes with commas, `rescue ArgumentError, KeyError => e`, or splat an array constant, `rescue *NETWORK_ERRORS => e`. Each entry must be a class or module; anything else raises `TypeError` ("class or module required for rescue clause") when an exception reaches that clause. `=> e` binds the exception to an ordinary local variable, which stays visible after the block ends.

code

ruby · 14 lines
ruby
require "json"
require "net/http"

NETWORK_ERRORS = [Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED].freeze

def usd_rate(http)
  JSON.parse(http.get("/rates").body).fetch("usd")
rescue *NETWORK_ERRORS => e
  warn "rates unavailable: #{e.class}"
  nil
rescue JSON::ParserError, KeyError => e
  warn "bad rates payload: #{e.message}"
  nil
end

go deeper

for a junior

Recall that the first matching rescue clause wins, so specific classes go before general ones.

for a middle

Explain matching with ===, comma lists and splatted arrays, and why a non-class in the list only fails when an exception arrives.

for a senior

Keep shared class lists in constants, avoid generic fallbacks that hide defects, and rely on Lint/ShadowedException and Lint/RescueType in CI.

for a principal

Design error groupings callers can rescue by meaning, such as a marker module across a library, rather than long class lists at every call site.

## How matching works When the body of a `begin` block (or a method or `do...end` body with rescue clauses) raises, Ruby searches its **rescue clauses from top to bottom**: 1. For each class or module listed in a clause, Ruby calls **`listed === exception`**. 2. For a class, `Module#===` is true when the exception is an instance of that class or a subclass, the same test as `exception.is_a?(listed)`. 3. The **first clause** with a true result runs. The syntax documentation is explicit: the exception is matched starting at the top, and matches only once. 4. If no clause matches, `ensure` runs and the exception propagates. Because the test is `===`, a **module** works too: if your gem's error classes include a marker module, `rescue ThatModule` catches all of them. Some classes customise `===`: according to its documentation, `SystemCallError.===` returns true when the receiver is the generic `SystemCallError`, or when the two error numbers are the same, so `Errno` matching is by error number. ## Why order matters Since only the first match runs, a broad class listed early **shadows** everything below it: | Clause order | ArgumentError raised | Result | |---|---|---| | `rescue ArgumentError` then `rescue StandardError` | first clause | specific handling | | `rescue StandardError` then `rescue ArgumentError` | first clause | generic handling; the second clause is dead code | Ruby does not warn about the dead clause. RuboCop's **`Lint/ShadowedException`** (enabled by default, and kept on by Standard) reports a broader class rescued before a narrower one, and **`Lint/DuplicateRescueException`** reports the same class listed twice. The rule of thumb is to order clauses **from most specific to most general**, and to end with the general clause only if you truly have a generic fallback. ## Class lists and splats One clause can handle several unrelated classes: - **Comma list**: `rescue Net::ReadTimeout, Errno::ECONNRESET => e`. - **Splatted array**: `rescue *NETWORK_ERRORS => e`, where `NETWORK_ERRORS` is a frozen array constant. This keeps a shared list in one place across several call sites. - **Without the splat**, `rescue NETWORK_ERRORS` passes an `Array`, which is not a class or module, and fails with `TypeError` once an exception arrives. ## What must be listed Every listed value must be a `Class` or `Module`. Ruby checks this lazily, at the moment an exception is being matched against that clause, and raises `TypeError` with the message `class or module required for rescue clause`. That means a typo such as `rescue "Timeout"` goes unnoticed until the first real failure, when it replaces the original error with a `TypeError`. RuboCop's `Lint/RescueType` flags rescue arguments that are literals such as strings, numbers or `nil`. ## Binding the exception `=> e` assigns the exception to a **local variable**. Two details surprise people: - `e` is an ordinary local of the surrounding method or block, so it is still defined after `end`; outside a rescue it keeps the last value assigned. - The global `$!` also holds the exception, but only while the rescue clause runs; afterwards it returns to its previous value, which is `nil` outside any rescue. Prefer the named variable. ## Example In a currency-rate fetcher, network failures return `nil` and a malformed payload is logged separately, while everything else propagates. The order puts the two specific groups first and adds no generic clause at all. ## What reviewers look for When reading a handler with several clauses, check these in order: - **Is the order specific to general?** A broad class above a narrow one is dead code, and usually a bug. - **Is each clause needed?** Two clauses with identical bodies can merge into one comma list. - **Is the list shared?** The same three network classes repeated at five call sites belong in one constant, splatted where used. - **Does a generic clause hide defects?** A final `rescue StandardError` in business code turns typos into handled errors; prefer letting unexpected errors reach the error tracker. - **Is every listed name a real class?** A misspelled constant raises `NameError` only when the clause is evaluated, which again is only when an exception arrives.

  • Is e still defined after the begin block ends?
    Yes. `=> e` assigns a normal local variable in the enclosing scope, so after `end` it still holds the rescued exception (or `nil` if the variable was created by the parser but never assigned). The global `$!`, in contrast, is restored to its previous value once the rescue clause completes.
  • Can a rescue clause match on a module instead of a class?
    Yes. Rescue calls `===` on each listed value, and `Module#===` is true for any object whose class includes that module. Tagging a library's error classes with a shared module lets callers `rescue` the module and catch all of them, whatever their superclasses.
  • When does Ruby detect a rescue clause that lists a string instead of a class?
    Only at run time, when an exception reaches that clause and Ruby tries to match it. It then raises `TypeError` with `class or module required for rescue clause`, replacing the original error. RuboCop's `Lint/RescueType` catches the literal case statically.

saying these in an interview costs you the question

  • Ruby picks the most specific matching rescue clause wherever it appears
  • Every rescue clause that matches runs in order
  • rescue NETWORK_ERRORS works without the splat
  • Ruby warns at parse time when a rescue clause is shadowed
  • The rescued variable e disappears after the begin block ends