throw and catch are not exceptions¶
Level: 201 · anyone who has used an exception to break out of two loops
One line: catch(:tag) { … throw :tag, value … } is a labelled non-local jump: throw unwinds to the matching catch, which returns the value, rescue clauses on the way never see it, ensure clauses still run — and only a throw with no matching catch becomes an exception, UncaughtThrowError.
Ruby keeps two mechanisms apart that Python folds into one. raise and rescue are for errors. throw and catch are for leaving early: a search that has found its answer three loops deep, a parser that has seen enough. catch takes a tag — nearly always a symbol — and runs its block; throw with that tag unwinds the stack to the catch, which then returns the thrown value (or nil). No exception object is built for a matched throw, which is why a rescue => e between the two is not consulted, while ensure clauses, which run on any exit, still do. The tag is matched by identity, which is the reason for symbols: two equal-looking strings are two objects. A throw that finds no catch above it has nowhere to go, and that is an exception — UncaughtThrowError, an ArgumentError, so a bare rescue catches it and reports the tag and value.
Verified output of throw_and_catch_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. catch returns the value thrown to it, or the block's value
with a throw: 20
without a throw: "block finished without a throw"
2. escaping nested loops
visiting 1,1
visiting 1,2
visiting 1,3
visiting 2,1
visiting 2,2
first cell whose product is 4: [2, 2]
3. a throw with no matching catch is an exception, and a StandardError
UncaughtThrowError: "uncaught throw :nope"; tag :nope, value 1
e.is_a?(ArgumentError) true
4. a matched throw is not seen by a rescue in between, but ensure runs
ensure ran on the way out
catch returned "thrown past the rescue"
5. catch with no tag makes one and hands it to the block
catch { |tag| throw tag, 7 } -> 7
the tag is a plain Object
6. throw with no value throws nil
catch(:t) { throw :t } -> nil
7. tags match by identity, which is why symbols are used
catch("s") { throw "s".dup, 1 } -> UncaughtThrowError: "uncaught throw \"s\""
the same String object -> 1
:s.equal?(:s) true
8. throw unwinds through method calls
catch(:bottom) { descend(5) } -> "reached the bottom"
Reading the output¶
Row 1 is the basic shape twice: with a throw the catch returns the thrown value, 20; without one it returns the block's last expression. Row 2 is the use case — two nested each loops, a throw as soon as the cell is found, and the visits stop at 2,2 with [2, 2] as the answer; no flag, no helper method, no exception.
Row 3 is the failure mode: throw :nope with no enclosing catch(:nope) raises UncaughtThrowError, whose message names the tag and whose tag and value readers hold both — and it is_a?(ArgumentError), hence a StandardError, hence caught by rescue => e. Row 4 is the difference from an exception: the throw passes a rescue => e without triggering it (no "rescue ran" line), but the ensure clause prints, and catch returns the thrown string — the block's own "block finished" is never reached.
Row 5 is the tagless form: catch with no argument creates a fresh Object as the tag and yields it, so a tag can be private to one block. Row 6: a throw with only a tag delivers nil. Row 7 explains the symbols: catch("s") does not catch throw "s".dup — two String objects, however alike, are two tags — while the same object works, and a symbol is always the same object (:s.equal?(:s)). Row 8 shows the jump crossing method boundaries: five nested calls of descend, one throw at the bottom, one catch at the top.
Compared with Python¶
Python has no second mechanism: a non-local jump is an exception, so the twin defines class Found(Exception) and a tiny catch(f) helper that calls f and returns the raised value. Rows 1, 2, 6 and 8 then print the same values — the exception carries the payload in args, unwinds through the loops and the recursion, and the helper hands it back. The differences are in what an exception is. Row 3: an unmatched Found is not a special error, just an uncaught Found. Row 4 is the one that matters in practice: an except Exception in between does intercept the jump, because Found is an Exception — the twin shows it swallowed and the function returning normally — and the only way to make a jump pass through such handlers is to derive it from BaseException (Escape), which is what SystemExit, KeyboardInterrupt and GeneratorExit do. finally runs in both cases, like ensure. Row 5: there is no tagless form, the class is the tag. Row 7: matching is by class (isinstance), never by identity, so two Found() instances match the same handler even though they are different objects. Python also has for … else for the commonest single-loop search: else runs only when the loop was not left by break. (Not machine-checked here.)
Verified output of throw_and_catch_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. a control-flow exception carries the value; the helper returns it
with a raise: 20
without a raise: 'function finished without a raise'
2. escaping nested loops
visiting 1,1
visiting 1,2
visiting 1,3
visiting 2,1
visiting 2,2
first cell whose product is 4: [2, 2]
3. a raise with no matching except is just an uncaught exception
Found; args (1,)
isinstance(e, Exception) True
4. an except Exception in between DOES intercept it; finally runs either way
except Exception intercepted Found
finally ran on the way out for Found
returned 'function finished'
finally ran on the way out for Escape
Escape (a BaseException) reached the outer handler
5. no tagless form: the exception class is the tag
any class works as a tag; there is nothing to generate
6. a raise with no value: args is empty, the helper returns None
catch(lambda: (_ for _ in ()).throw(Found())) -> None
7. matching is by class, not identity
isinstance(Found(), Found) True; Found() is Found() False
8. the jump unwinds through function calls
catch(lambda: descend(5)) -> 'reached the bottom'
Try it¶
- Write
find_pair(matrix, target)that returns the[row, column]of the first cell equal totarget, printing each cell it visits, usingcatchandthrowto leave both loops at once, and returningnilwhen the value is absent. Run it on a 3×3 matrix for a present and an absent value. - Wrap the search in
begin … rescue => e … ensure … endand show that a matchedthrowskips therescuebut runs theensure; thenthrowa tag nobody catches and show therescuecatchingUncaughtThrowErrorand printinge.tag.
Solution to 1
throw_and_catch_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: find_pair returns the [row, column] of the first cell equal to target,
# leaving both loops the moment it is found, and nil when the value is absent.
def find_pair(matrix, target)
catch(:found) do
matrix.each_with_index do |row, r|
row.each_with_index do |value, c|
puts " visiting #{r},#{c} (#{value})"
throw :found, [r, c] if value == target
end
end
nil
end
end
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
puts "1. a value that is present"
puts " -> #{find_pair(matrix, 5).inspect}"
puts "2. a value that is absent"
puts " -> #{find_pair(matrix, 42).inspect}"
Verified output of throw_and_catch_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. a value that is present
visiting 0,0 (1)
visiting 0,1 (2)
visiting 0,2 (3)
visiting 1,0 (4)
visiting 1,1 (5)
-> [1, 1]
2. a value that is absent
visiting 0,0 (1)
visiting 0,1 (2)
visiting 0,2 (3)
visiting 1,0 (4)
visiting 1,1 (5)
visiting 1,2 (6)
visiting 2,0 (7)
visiting 2,1 (8)
visiting 2,2 (9)
-> nil
See also¶
next,breakandreturnin blocks —breakleaves one loop;throwleaves as many as you likeforloops do not scope — the loops being escaped, and what they leave behindrescue,elseandensurerun in a fixed order — theensurethat row 4 shows running for athrow- A bare
rescuecatchesStandardError, not everything — whyrescue => ecatches anUncaughtThrowError - Kernel#catch ↗ — the reference, with
throwbeside it - Common error messages — Twenty-nine errors you will meet in your first month of Ruby, each with the code that raises it, the…