skip to content

In Ruby, how do `catch`/`throw` differ from `raise`/`rescue`, and what happens when `throw` finds no matching `catch`?

level: seniorimportance: should knowfreq 30%

answer

  1. expected exit versus error
  2. tag matched by identity
  3. rescue skipped, ensure runs
  4. no exception object or backtrace
  5. UncaughtThrowError < ArgumentError, tag and value

basics

~20 s

Ruby's throw is non-error unwinding to the catch with the same tag object, handing it a value and running ensure clauses; rescue never sees it. With no matching catch, throw raises UncaughtThrowError, an ArgumentError subclass.

solid answer

~50 s

`catch(tag) { ... }` and `throw tag, value` are `Kernel` methods for **expected** non-local exits, while `raise`/`rescue` are for errors. `throw` searches the active `catch` blocks for one whose tag is the **same object** — a Symbol, or the fresh object `catch` yields when called without a tag — and unwinds straight to it; `catch` then returns `value` (default `nil`), or its block's last value if nothing was thrown. On the way, `ensure` clauses run but `rescue` clauses do not intercept it, and when a `catch` matches no exception object or backtrace is built. If no `catch` matches, `throw` raises `UncaughtThrowError` at the throw site; it inherits from `ArgumentError`, so a bare `rescue` catches it, and it exposes `#tag` and `#value`. Use `throw` for early exit from deep traversal, and `raise` when something went wrong.

code

ruby · 14 lines
ruby
def pick(bins)
  bins.each do |bin|
    begin
      throw :picked, bin if bin == :b2
    rescue Exception
      puts "never printed"
    ensure
      puts "closed #{bin}"
    end
  end
end

p catch(:picked) { pick([:b1, :b2, :b3]); nil }
# prints closed b1, closed b2; => :b2

go deeper

for a junior

Recall that raise and rescue handle errors, while catch and throw jump to a tagged exit and hand it a value.

for a middle

Explain identity matching of tags, what catch returns in both cases, and that ensure runs while rescue is skipped.

for a senior

Choose throw for expected deep exits and raise for failures, and know that an unmatched tag surfaces as UncaughtThrowError with tag and value.

for a principal

Keep throw inside a module's own traversal code so no caller outside it ever needs to know the tag exists.

## Two unwinding mechanisms Ruby has two ways to jump out of several nested calls at once, and they are easy to confuse because other languages spell exceptions `throw`/`catch`. - **`raise` / `rescue`** — exceptions. An `Exception` object is created, carries a message and a backtrace, and is matched by class in `rescue` clauses. It signals that something **went wrong**. - **`catch` / `throw`** — tagged non-local exit. There is no exception object when a matching `catch` exists; `throw` carries a tag and a value. It signals an **expected** change of control flow, such as "found it, stop searching". The language reference draws the same line: `throw`/`catch` are for expected non-local control flow, exceptions for exceptional situations. ## How `catch` and `throw` match ```ruby result = catch(:done) do walk_tree(root) # somewhere deep inside: throw :done, node :not_found end ``` The rules, from `Kernel#catch` and `Kernel#throw`: 1. `catch(tag)` runs its block and yields `tag` to it. Without an argument, `catch` creates a new unique object (as `Object.new`) and yields that, which guarantees no other code can throw to it by accident. 2. `throw(tag, value = nil)` looks for an active `catch` whose tag is the **same object** (same `object_id`). Equality is not enough: two separately built strings with the same text do not match. 3. When found, execution resumes at the end of that `catch`, and `catch` returns `value`. 4. If the block finishes without a matching `throw`, `catch` returns the block's last value. Symbols are the usual tag because the same symbol is always the same object. ## What happens on the way out As `throw` unwinds: - **`ensure` clauses run**, so files close and locks release as they would for an exception. - **`rescue` clauses do not intercept it**, not even `rescue Exception`, because no exception is being raised when a catch matches. - Frames between `throw` and `catch` simply stop; code after the call that led to `throw` does not run. ## When nothing matches If no active `catch` has the tag, `throw` raises **`UncaughtThrowError`** from the `throw` call: ```ruby begin throw :nope, 3 rescue UncaughtThrowError => e e.tag # => :nope e.value # => 3 e.message # => "uncaught throw :nope" end ``` `UncaughtThrowError` is a subclass of **`ArgumentError`**, and therefore of `StandardError`. Unlike a matched throw, it **is** an ordinary exception: a bare `rescue` or `rescue ArgumentError` between the `throw` and the top level will catch it. The design reasoning in the reference is that throwing a tag nobody expects is a programming error. ## Side-by-side | Aspect | `catch` / `throw` | `raise` / `rescue` | |---|---|---| | Intended for | expected early exit | errors | | Matched by | tag object identity | exception class (`===`) | | Payload | any value, returned by `catch` | an exception object | | Backtrace | none built when a catch matches | captured at `raise` | | `ensure` runs | yes | yes | | Intercepted by `rescue` | no (unless no catch matches) | yes | | Unmatched | `UncaughtThrowError` | unhandled, it ends the thread or program | ## A traversal example A parser that validates a deeply nested configuration can use `throw` to stop at the first fatal problem without threading a status value through every recursive call: ```ruby def check(node, path) throw :invalid, path if node.nil? node.each { |key, child| check(child, path + [key]) } if node.is_a?(Hash) end bad_path = catch(:invalid) { check(config, []); nil } ``` `bad_path` is `nil` when the whole tree is valid and the path to the first missing value otherwise. No exception object is created on the fast path, and any `ensure` inside the traversal still runs. If the problem were unexpected — a file that cannot be read — `raise` would be the right tool. ## Practical notes - Both are **methods on `Kernel`**, not keywords. Inside a `BasicObject` context, where `Kernel` is not included, call `::Kernel.catch` and `::Kernel.throw`. - Because a matched `throw` builds no exception object or backtrace, it avoids the cost that `raise` pays for them; that makes it suitable for frequent early exits, such as a parser that bails out of a deeply nested descent. - Do not use `throw` to report failures. Callers expect failures to arrive as exceptions they can `rescue` by class, with a backtrace to debug. - Prefer a `return` from a named method when the exit stays within one method; reach for `catch`/`throw` when it must cross method calls.

  • Why does catch("done") { throw "done" } raise UncaughtThrowError in Ruby 4.0 without a frozen_string_literal comment?
    Tags are matched by object identity. Without the magic comment, each string literal evaluation produces a new String (chilled, but distinct), so the thrown string is not the same object as the caught one. Use a Symbol, or the object `catch` yields when called without a tag.
  • Can a rescue clause between throw and catch ever see anything from a throw?
    Only when no `catch` matches. A matched `throw` unwinds without raising, so `rescue` never fires. An unmatched `throw` raises `UncaughtThrowError`, a `StandardError` via `ArgumentError`, and any enclosing `rescue` for those classes will catch it.

catch(:found) is a supervisor waiting at the warehouse exit holding one specific ticket. A worker deep in the aisles who calls throw walks straight to the supervisor holding that exact ticket, closing each door behind them (ensure) and setting off no alarm (no exception). A worker holding a ticket no supervisor has is the only one who trips the alarm: UncaughtThrowError.

saying these in an interview costs you the question

  • throw and catch are Ruby's way of raising and handling errors
  • rescue Exception between throw and catch will intercept the throw
  • A throw skips the ensure clauses it unwinds through
  • catch matches any tag that is == to its own
  • An unmatched throw fails silently and returns nil