Kernel#catch
catch(tag) { block } -> value The catch method establishes a block that can receive transfers of control from throw. Unlike exceptions, it’s designed for normal control flow, allowing early exit from nested structures.
Basic Usage
result = catch(:done) do
throw :done if some_condition
"completed"
end
The block returns its last expression if no throw fires, or the value passed to throw if one does. This makes catch useful as a way to say “return this early” from inside a nested block without resorting to exceptions or break statements that only escape one level.
Practical Examples
Early exit from nested loops
result = catch(:found) do
(1..10).each do |x|
(1..10).each do |y|
if x == 5 && y == 5
throw :found, [x, y]
end
end
end
nil
end
puts result # => [5, 5]
The throw call carries an optional second argument that becomes the return value of the catch block. This lets you return a computed value from deep inside the loop, not just signal an early exit. The nesting is straightforward to follow because the throw only targets the nearest matching catch with that tag.
Finding Items
def find_admin(users)
catch(:admin_found) do
users.each do |group|
group.each do |user|
throw(:admin_found, user) if user[:admin]
end
end
nil
end
end
A catch block wraps the entire search, and when the target is found the method throws back the result. Because the tag is unique to that particular search, nested catch blocks with different tags do not interfere with each other.
State Machine
def process_state_machine
state = :initial
catch(:terminate) do
loop do
case state
when :initial
state = :processing
when :processing
state = :complete
when :complete
throw :terminate, "Success"
end
end
end
end
The state machine pattern uses catch as a named exit point, which is cleaner than checking a termination flag on every iteration. The tag name :terminate makes the intent explicit: the loop runs until the state machine decides it is finished.
Return Value
# Returns block's last value if no throw
result = catch(:tag) { "normal exit" }
puts result # => "normal exit"
# Returns thrown value if throw executes
result = catch(:tag) { throw :tag, "early exit" }
puts result # => "early exit"
When throw fires, the method unwinds the stack back to the matching catch and returns the thrown value. If no throw matches, Ruby raises an UncaughtThrowError, which is why the tag must match exactly between the catch and the throw.
Nested catch Blocks
catch(:outer) do
catch(:inner) do
throw :outer, "skipped inner"
end
"inner block"
end
# => "skipped inner"
When catch blocks are nested, a throw only targets the innermost matching catch. If the outer tag is thrown, the inner block is abandoned entirely and its return value is never used. This gives you fine-grained control over which layer of logic handles the early exit.
Tag Types
# Tags can be symbols
catch(:symbol) { }
# Or strings
catch("string_tag") { }
Symbols are more common as tags because they are lightweight and idiomatic, but strings work just as well. The tag is compared by object identity for symbols and by content equality for strings, so "tag" matches "tag" but two different String objects with the same text also match.
Comparison with Exceptions
# catch/throw - for flow control
catch(:exit) { throw :exit, result }
# raise/rescue - for error handling
begin
raise "Error"
rescue => e
# Handle error
end
Use Cases
- Breaking out of nested loops cleanly
- Implementing state machines
- Early exit from complex algorithms
- Search operations
- Game AI (state transitions)
using catch for control flow
catch is a good fit when the code needs a non-local exit that is still part of normal control flow. It can make deeply nested search or state handling easier to follow because the code says “stop here and return this value” without turning the situation into an error. That works well when the early exit is expected rather than exceptional.
The key is to keep the tag meaningful and the scope small. A short, readable tag makes the structure obvious, and a narrow catch block keeps the transfer of control easy to reason about. When the logic starts to look like error handling, though, a real exception may be the clearer tool.
keeping tags easy to follow
The tag you choose for catch should tell the reader what kind of early exit it represents. That way the block feels like a named boundary instead of a hidden jump. In a small search or state machine, that can make the control flow much easier to trace, especially when the rest of the method is already doing several jobs. If the tag starts to feel mysterious, the code probably wants a smaller method or a clearer return path.
keeping the exit path readable
catch is clearest when the code that throws the value is close enough for the reader to see the whole path at once. That way the flow still feels deliberate instead of magical. In a search loop, a small state machine, or a helper with a single early exit, the pattern can be neat and compact. When the jump starts to cross too many layers, a direct return or a smaller method is often easier to understand.