In Ruby, how do `catch`/`throw` differ from `raise`/`rescue`, and what happens when `throw` finds no matching `catch`?
answer
- expected exit versus error
- tag matched by identity
- rescue skipped, ensure runs
- no exception object or backtrace
- UncaughtThrowError < ArgumentError, tag and value
basics
~20 sRuby'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 linesdef 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; => :b2go deeper
Recall that raise and rescue handle errors, while catch and throw jump to a tagged exit and hand it a value.
Explain identity matching of tags, what catch returns in both cases, and that ensure runs while rescue is skipped.
Choose throw for expected deep exits and raise for failures, and know that an unmatched tag surfaces as UncaughtThrowError with tag and value.
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