skip to content

In Ruby, which exceptions does a bare rescue clause catch, and which built-in exceptions slip past it?

level: juniorimportance: must knowfreq 76%

answer

  1. one root class, two big branches
  2. rescue with no class list
  3. StandardError and every subclass
  4. Interrupt, SystemExit, ScriptError outside
  5. rescue => e has the same scope

basics

~10 s

A bare rescue, like rescue => e, catches StandardError and its subclasses only. Classes outside that branch pass through: SignalException and its subclass Interrupt, SystemExit, ScriptError (LoadError, SyntaxError, NotImplementedError), NoMemoryError and SystemStackError.

solid answer

~40 s

Every Ruby exception descends from `Exception`, but a `rescue` with no class list defaults to `StandardError`, and it matches that class and all its subclasses. That covers everyday failures: `ArgumentError`, `TypeError`, `NameError` and `NoMethodError`, `KeyError`, `ZeroDivisionError`, `RuntimeError` (what `raise "msg"` creates) and every `Errno::*` class through `SystemCallError`. `rescue => e` only adds a variable; its scope is identical. What slips past are the siblings of `StandardError` under `Exception`: `SignalException` and its subclass `Interrupt` (Ctrl-C), `SystemExit` (raised by `exit`), `ScriptError` with `LoadError`, `SyntaxError` and `NotImplementedError`, `NoMemoryError`, `SystemStackError` and `SecurityError`. Those are shutdown signals or problems a local handler rarely can fix, which is why the default leaves them alone.

code

ruby · 19 lines
ruby
def parse_quantity(text)
  Integer(text)
rescue => e                 # same scope as rescue StandardError => e
  warn "bad quantity: #{e.class}"
  0
end

parse_quantity("12")   # => 12
parse_quantity("abc")  # ArgumentError is a StandardError: warns, returns 0

begin
  begin
    raise Interrupt        # the class Ctrl-C raises in the main thread
  rescue => e
    puts "not reached"
  end
rescue Interrupt => e
  puts "escaped the bare rescue: #{e.class}"
end

go deeper

for a junior

Recall the one rule: no class list means StandardError and its subclasses. Be ready to name two or three classes that are not caught, such as Interrupt and SystemExit.

for a middle

Explain the tree shape: StandardError is one branch under Exception, beside SignalException, SystemExit, ScriptError, NoMemoryError and SystemStackError, and matching follows inheritance.

for a senior

Show that you rely on the narrow default on purpose: handlers stay killable and missing dependencies stay loud, and you name a non-StandardError class only where you mean to handle it.

for a principal

Frame the default as a policy boundary: application code handles StandardError, while signals, exit and load failures belong to process-level code that owns startup and shutdown.

## One root, two kinds of failure In Ruby every exception object is an instance of **`Exception`** or one of its subclasses. Directly under `Exception` the tree splits into two kinds of branch: - **`StandardError`** and everything below it: the errors an application is expected to handle, such as bad arguments, missing hash keys, failed system calls and division by zero. - **Several sibling classes** that also inherit straight from `Exception` but are *not* `StandardError`: signals, process exit, script loading failures, memory exhaustion and stack overflow. A **bare `rescue`** is a rescue clause with no class list: `rescue` on its own, or `rescue => e`. The language reference for exceptions states the rule directly: if no class is given, `StandardError` is assumed, and the clause rescues that class or any of its subclasses. ## What a bare rescue catches The `StandardError` subtree is large. The classes you meet most often: | Class | Parent | Typical trigger | |---|---|---| | `ArgumentError` | `StandardError` | wrong argument count, `Integer("abc")` | | `TypeError` | `StandardError` | `1 + "2"`, `Integer(nil)` | | `NameError` | `StandardError` | undefined local variable or constant | | `NoMethodError` | `NameError` | calling a method the receiver lacks | | `KeyError` | `IndexError` | `Hash#fetch` or `ENV.fetch` on a missing key | | `ZeroDivisionError` | `StandardError` | integer division by zero | | `RuntimeError` | `StandardError` | `raise "message"` with no class | | `FrozenError` | `RuntimeError` | mutating a frozen object | | `Errno::ENOENT` and friends | `SystemCallError` | failed operating-system calls | | `IOError` | `StandardError` | reading a closed stream | Because matching follows inheritance, a bare rescue catches every row of that table. `rescue => e` is not broader or narrower: it is the same default plus a variable binding. ## What slips past These built-in classes inherit from `Exception` but not from `StandardError`, so a bare rescue never sees them: 1. **`SignalException`** and its subclass **`Interrupt`**. By default, pressing Ctrl-C makes Ruby raise `Interrupt` in the main thread, and signals such as TERM raise a `SignalException`. 2. **`SystemExit`**, raised by `exit`. Letting it pass is what makes `exit` inside a method actually end the program. 3. **`ScriptError`** with **`LoadError`** (a failed `require`), **`SyntaxError`** and **`NotImplementedError`**. The class documentation says outright that these are not `StandardError` and are rescued only when named explicitly. 4. **`NoMemoryError`**, raised when memory allocation fails. 5. **`SystemStackError`**, raised on stack overflow ("stack level too deep"). 6. **`SecurityError`**, which the documentation now describes as no longer used by internal code. There is also an internal class named `fatal` (lowercase) that Ruby uses when it must exit; application code does not reference it. ## Why the default is StandardError The split is deliberate. The classes outside `StandardError` mean either **someone wants the process to stop** (a signal, `exit`) or **the program itself is broken in a way a local handler cannot repair** (code that does not parse, a library that is not installed, exhausted memory). A generic handler written to recover from a failed parse of user input should not also swallow the user pressing Ctrl-C. Keeping the default narrow means ordinary error handling cannot accidentally make a program unkillable or hide a missing dependency. ## Inspecting a class's position When you are unsure where a class sits, ask Ruby: - `ZeroDivisionError.superclass` returns `StandardError`. - `Interrupt.superclass` returns `SignalException`, whose superclass is `Exception`. - `Module#ancestors` lists the whole chain, including any modules mixed in along the way. - `e.is_a?(StandardError)` tells you, for a caught exception, whether a bare rescue would have seen it. In an order-import script, for example, a bare rescue around parsing one row catches `ArgumentError` from `Integer("abc")` and `KeyError` from `fetch`, logs them and moves to the next row, while Ctrl-C still stops the whole run. ## Common misreadings Interviewers probe the rule with near-misses. The ones that come up most: - **"A bare rescue catches everything."** It catches `StandardError` only; Ctrl-C, `exit` and a failed `require` all pass through. - **"`rescue => e` is the catch-all form."** The variable changes nothing about scope. - **"Only `RuntimeError` is caught."** `RuntimeError` is what `raise "text"` creates, but it is just one of many `StandardError` subclasses the default covers. - **"Custom errors are caught automatically."** Only when they inherit from `StandardError`; a class that inherits from `Exception` directly escapes every bare rescue, which is why custom error classes normally subclass `StandardError`. The quick self-test is to picture the tree: if the path from the class up to `Exception` passes through `StandardError`, a bare rescue sees it; otherwise it does not.

  • Does rescue => e catch anything that a bare rescue does not?
    No. `rescue => e` uses the same default class, `StandardError`, and matches the same subclasses. The only difference is that the exception object is bound to `e` for the clause body. To widen the scope you must name a class, for example `rescue LoadError` or `rescue Exception`.
  • Where do Errno::ENOENT and KeyError sit in the tree?
    `Errno::ENOENT` inherits from `SystemCallError`, which inherits from `StandardError`. `KeyError` inherits from `IndexError`, also a `StandardError`. So a bare rescue catches a missing file and a missing hash key alike, and `rescue IndexError` would catch `KeyError` too.
  • How do you check whether a bare rescue would catch a given class?
    Ask the class: `SomeError <= StandardError` returns `true` when it is `StandardError` or a subclass, and `SomeError.ancestors` shows the full chain. For an exception object, `e.is_a?(StandardError)` answers the same question.

saying these in an interview costs you the question

  • A bare rescue catches every exception, including Ctrl-C
  • rescue => e is broader than a bare rescue
  • Interrupt is a subclass of StandardError
  • A bare rescue catches only RuntimeError
  • A bare rescue handles LoadError from a failed require