rubyguides

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.

See Also