A greenhouse controller's `loop do` that polls a flaky sensor stops with no error and no log line — how can Ruby's Kernel#loop cause that?
answer
- loop is a method, not a keyword
- rescues StopIteration
- returns StopIteration#result
- block opens a new scope
- Style/InfiniteLoop is marked unsafe
basics
~10 sKernel#loop rescues StopIteration raised anywhere in its block and returns that exception's result, so an exhausted enumerator deep in the polling code ends the loop as if it had finished normally.
solid answer
~40 s`loop` is a `Kernel` method, not a keyword: it yields its block forever and wraps that in `rescue StopIteration`, returning the exception's `result`. If any code called from the body raises `StopIteration` — typically `Enumerator#next` on an exhausted enumerator, such as a calibration table the driver walks — the loop ends quietly, `loop` returns `nil` or the iterator's final value, and the controller carries on as if polling were done. `while true` does not do this; the exception would propagate and be logged. Fixes: rescue `StopIteration` at the call that can exhaust, use `while true` where an escaping exception is the correct outcome, and log after the loop. RuboCop's `Style/InfiniteLoop` autocorrect is marked unsafe for exactly this reason. `loop` also opens a block scope, unlike `while`.
code
ruby · 12 linesresult = loop do
raise StopIteration
end
p result # => nil, and no error reached the caller
begin
while true
raise StopIteration
end
rescue StopIteration => e
p e.class # => StopIteration, it propagated out of while
endgo deeper
Recall that loop is a Kernel method taking a block, runs forever until something stops it, and ends quietly on StopIteration.
Explain the rescue inside loop, what StopIteration#result is, and the scope difference between a loop block and a while body.
Diagnose a silently stopped worker by tracing enumerator exhaustion under a loop, and fix it where the exception originates rather than around the loop.
Decide when a polling loop should be allowed to end at all, and make every unexpected exit observable through logging or supervision.
## `loop` is a method with a rescue inside Ruby has keywords for loops (`while`, `until`, `for`) and one very common **method**: `Kernel#loop`. In Ruby 4.0 it is written in Ruby, in `kernel.rb`, and its body is essentially: ```ruby def loop return to_enum(:loop) unless block_given? # simplified begin while true yield end rescue StopIteration => e e.result end end ``` Three behaviours follow directly from that source: 1. **It rescues `StopIteration`.** Any `StopIteration` raised while the block runs — directly or from any method it calls — ends the loop without an error. 2. **It returns the exception's `result`.** `StopIteration#result` is the value the underlying iterator returned; for a plain `raise StopIteration` it is `nil`. 3. **Without a block it returns an `Enumerator`** whose `size` is `Float::INFINITY`. `StopIteration` is the exception `Enumerator#next` raises when an external enumerator runs out. The rescue is deliberate: it lets `loop { puts enum.next }` stop cleanly at the end of `enum`. ## How it hides a failure Picture a greenhouse controller: ```ruby loop do raw = sensor.read temp = calibrator.apply(raw) # walks a curve with Enumerator#next vents.adjust(temp) sleep 5 end logger.info("polling finished") # the only trace ``` If `calibrator.apply` walks its curve with `next` and a reading falls outside the table, `Enumerator#next` raises `StopIteration`. Nothing between that call and `loop` rescues it, so `loop` does. The daemon leaves the polling loop, `loop` returns the enumerator's final value, and the vents stop adjusting. There is no stack trace, because from Ruby's point of view nothing failed. ## `loop` versus `while true` | Aspect | `loop do ... end` | `while true ... end` | |---|---|---| | What it is | `Kernel` method taking a block | keyword | | `StopIteration` from the body | rescued; loop returns its `result` | propagates to the caller | | Variables first assigned inside | local to the block, gone afterwards | visible after the loop | | Without a body | returns an infinite `Enumerator` | not applicable | | Leaving early | `break` works in both | `break` works in both | The scope row matters in retry code. A variable such as `reading` that is first assigned inside `loop do` is not visible after the loop; declare it before the loop if later code needs it. ## RuboCop knows about this - `Style/InfiniteLoop` suggests replacing `while true` with `loop do`. Its autocorrection is marked **unsafe**, and the cop's own documentation gives the reason: the rewrite changes behaviour if the body might raise `StopIteration`. - `Lint/Loop` suggests `loop do ... break ... end` in place of `begin ... end while`, and its autocorrection is unsafe for the same two reasons: `StopIteration` handling and variables that were visible after the old loop. ## Diagnosing and fixing it When a `loop`-based worker stops without an error, check for `StopIteration` first: 1. **Log after the loop.** A daemon loop that should never finish deserves a line after it that records its return value; reaching that line is the bug. 2. **Find the enumerator.** Search the body's call path for external enumerators (`next`, `peek`) and for any code that raises `StopIteration` by hand. 3. **Handle exhaustion where it happens.** Rescue `StopIteration` at the `next` call and turn it into a domain error (`CalibrationRangeError`) or a clamped value, so the outer loop never sees it. 4. **Choose the construct on purpose.** If an escaping exception is the right outcome for this loop, `while true` states that intent; if you want enumerator exhaustion to end the loop, `loop` states that. ## A bounded retry that does not rely on the rescue For the flaky read itself, a counted retry keeps the intent explicit: ```ruby reading = nil 3.times do reading = sensor.read break if reading sleep 0.5 end raise SensorTimeout, "no reading" unless reading ``` `Integer#times` bounds the attempts, the variable is declared outside the block so it survives, and a failed read raises an error of your own rather than disappearing. ## Key points - `loop` is not a keyword; it is a method with a `rescue StopIteration` inside. - `StopIteration` from anywhere under the block ends the loop quietly and becomes its return value. - `while true` propagates it and keeps body locals visible afterwards. - Treat a silent exit of a daemon loop as a `StopIteration` suspect until proven otherwise.
- What does Ruby's loop return when the enumerator whose next raised StopIteration came from [1, 2].each?It returns `[1, 2]`. `loop` returns `StopIteration#result`, which is the value the underlying iteration method returned when it finished, and `Array#each` returns its receiver. For a bare `raise StopIteration` the result is `nil`.
- Why is a variable first assigned inside loop do not visible after the loop, when the same code in a while loop is?`loop` is a method call with a block, and a block opens a new local-variable scope. `while` is a keyword whose body shares the enclosing scope. Assign the variable before the loop, for example `reading = nil`, if later code must read it.
saying these in an interview costs you the question
- loop is a keyword, so it behaves exactly like while true
- Kernel#loop re-raises StopIteration so the caller sees it
- Variables first assigned in a loop do block stay visible after it
- A daemon loop that exits without a stack trace cannot have hit an exception
- RuboCop's while true to loop rewrite is always behaviour-preserving