skip to content

In Ruby, when nested `each` blocks search a warehouse's aisles and bins, how do you stop every level at the first matching bin?

level: middleimportance: should knowfreq 40%

answer

  1. break reaches one level only
  2. catch with a tag, throw to it
  3. catch returns the thrown value
  4. return from an extracted method
  5. or a query method instead

basics

~20 s

A Ruby break ends only the innermost each, so outer levels keep going. Wrap the loops in catch(:found) and throw :found, bin, extract them into a method that returns the bin, or rewrite the search as a single find.

solid answer

~40 s

`break` terminates only the method that yielded to the current block — the innermost `each` — so the outer `each` over aisles continues with the next aisle. Three idiomatic ways stop everything at once. First, `catch(:found) do ... end` around the loops and `throw :found, [aisle, bin]` at the match: `catch` returns the thrown value, or the block's last value if nothing was thrown, so end the block with `nil` for the miss. Second, move the loops into a method and `return [aisle, bin]` from inside the blocks; the method's final `nil` covers the miss. Third, avoid nesting: flatten the candidates, for example with `each_with_index` and `flat_map`, and call `find`. A flag variable checked after every inner loop also works, but it spreads one decision across every level.

code

ruby · 11 lines
ruby
layout = [[:bolt, :nut], [:valve, :valve]]

hit = catch(:found) do
  layout.each_with_index do |aisle, a|
    aisle.each_with_index do |part, b|
      throw :found, [a, b] if part == :valve
    end
  end
  nil
end
p hit   # => [1, 0]

go deeper

for a junior

Recall that break stops only the innermost block and that catch with a matching throw leaves every level at once.

for a middle

Explain what catch returns when something is thrown and when nothing is, and compare it with extracting a method that returns.

for a senior

Pick the exit technique by readability and scope: a named method first, catch and throw when the exit crosses method calls you cannot restructure.

for a principal

Encourage search code with a single defined miss value so nested traversals do not each invent their own exit convention.

## Why `break` is not enough A warehouse layout is a nested structure: aisles contain shelves, shelves contain bins. A search for the first bin holding a part is naturally a nested loop: ```ruby layout.each_with_index do |aisle, a| aisle.each_with_index do |bin, b| break if bin.part == :valve # stops only this aisle's loop end end ``` In Ruby, `break` inside a block terminates **the method that yielded to that block**. Here that is the inner `each_with_index`. The outer loop carries on with the next aisle, and a second matching bin later in the warehouse would be "found" again. There is no labelled `break` in Ruby; each `break` reaches exactly one level. ## Option 1: `catch` and `throw` `Kernel#catch` runs a block and waits for a matching `Kernel#throw`. `throw` unwinds through any number of blocks and method calls to that `catch`: ```ruby location = catch(:found) do layout.each_with_index do |aisle, a| aisle.each_with_index do |bin, b| throw :found, [a, b] if bin.part == :valve end end nil # value when nothing was thrown end ``` Rules that matter here: - `catch` returns the **second argument of `throw`** (or `nil` if `throw` had only the tag). - If no `throw` happens, `catch` returns the **last value of its block**, so an explicit `nil` makes the miss clear. - The tag is matched by object identity, so a **Symbol** such as `:found` is the usual choice. - `ensure` clauses in the code being unwound still run. ## Option 2: extract a method and `return` ```ruby def locate(layout, part) layout.each_with_index do |aisle, a| aisle.each_with_index do |bin, b| return [a, b] if bin.part == part end end nil end ``` `return` inside these blocks returns from `locate` itself, leaving every loop at once. This is often the clearest option: the search gets a name, and its result is the method's return value. It works because the blocks run while `locate` is still active; the rules for `return` in procs and lambdas are a separate topic. ## Option 3: remove the nesting Often the nested loop is only there to produce candidates. Generate them first and ask a query method: ```ruby spots = layout.each_with_index.flat_map do |aisle, a| aisle.each_with_index.map { |bin, b| [bin, a, b] } end bin, a, b = spots.find { |candidate, _, _| candidate.part == :valve } ``` This builds every candidate before searching, which is fine for a small layout. For a large one, a lazy enumerator avoids it, at the cost of more machinery. ## Choosing between them | Approach | Stops all levels | Miss value | Best when | |---|---|---|---| | `break` | no, innermost only | the inner call's value | one level of looping | | flag variable + `break` per level | yes, with a check at each level | whatever you set | never preferred; noisy | | `catch`/`throw` | yes | block's last value | the loops must stay inline, or the exit is several method calls deep | | method + `return` | yes | the method's final value | the search deserves a name (usually) | | flatten + `find` | not needed | `nil` | candidates are cheap to enumerate | ## Pitfalls - **Forgetting the miss value.** Without the trailing `nil`, a `catch` block that throws nothing returns whatever its last expression gave, here the result of the outer `each`: the whole layout. - **Reusing a tag.** Two nested `catch(:found)` blocks are legal, and `throw :found` reaches the innermost one. Use distinct tags, or the object `catch` yields when called without a tag, when searches nest. - **Using `throw` for failures.** A bin that cannot be read is an error; raise an exception for it and keep `throw` for "found it". - **Returning from a block stored for later.** The extract-a-method option relies on the block running while the method is active; do not keep such a block and call it after the method has returned. ## What interviewers listen for 1. You know `break` reaches one level only. 2. You can name `catch`/`throw` and say what `catch` returns in the found and not-found cases. 3. You reach for a method with `return`, or a query method, before a flag variable. 4. You keep `throw` for expected control flow, not for errors.

  • In Ruby, what does catch(:found) return if the block finishes without any throw :found?
    It returns the value of the block's last expression, exactly like a method body. That is why a search wrapped in `catch` usually ends its block with `nil`: otherwise the miss case returns whatever the outer `each` returned, which is the layout array itself.
  • Why can't you pass break a label or a count in Ruby to leave two loops at once?
    Ruby has no labelled or numbered `break`. The argument of `break` is a value, not a target: `break :outer` simply makes the innermost yielding method return the symbol `:outer`. Leaving several levels needs `catch`/`throw`, a `return` from an extracted method, or a restructured search.

saying these in an interview costs you the question

  • break inside the inner block stops the outer each as well
  • break :outer jumps to a loop labelled outer
  • raise StopIteration to escape nested each calls
  • catch returns nil when no throw happens
  • A flag variable is the idiomatic Ruby way to leave nested loops