In Ruby, why does a form-validation rule built in a method as proc { |v| return false if v.nil?; true } raise LocalJumpError later?
answer
- return needs a live method
- orphaned proc
- "unexpected return" message
- reason and exit_value
- break from proc-closure
basics
~20 sA regular proc's return targets the method that created it. Once that method has returned the proc is orphaned, so return raises LocalJumpError (unexpected return). Build the rule as a lambda, or use next false, to leave only the callable.
solid answer
~40 sA `return` inside a regular proc does not end the proc; it ends the method the proc was written in. The rule was created inside a configuration method that returned long ago, so when the form calls it with `nil` that `return` has no live frame to unwind to and Ruby raises `LocalJumpError` with the message `unexpected return`; `reason` is `:return` and `exit_value` is `false`. The rule only fails on the path that executes `return`, which is why it can pass tests with present values. The fix is to build rules as lambdas, where `return` ends the lambda, or to write `next false`. `break` has the same trap: `[1, 2].each(&proc { break })` raises `break from proc-closure`, because the block was attached to the `proc` call, not to `each`.
code
ruby · 19 linesclass SignupForm
def self.presence_rule
proc { |value| return false if value.nil?; true }
end
end
rule = SignupForm.presence_rule # presence_rule has returned
rule.call("ann") # => true, return never runs
begin
rule.call(nil)
rescue LocalJumpError => e
e.message # => "unexpected return"
e.reason # => :return
e.exit_value # => false
end
safe = ->(value) { return false if value.nil?; true }
safe.call(nil) # => falsego deeper
Recall that return inside a proc leaves the method the proc was written in, while return inside a lambda leaves only the lambda.
Explain orphaned procs: once the creating method has returned, return raises LocalJumpError with unexpected return, and break has its own target.
Diagnose it from a backtrace, check lambda? on the stored object, fix the rule at its creation site, and add a spec that exercises the failing branch.
Set the convention for registries of stored callables: build them as lambdas, document the semantics callers get, and treat captured caller blocks with care.
## What return means inside a regular proc Ruby has two kinds of `Proc` object. A **lambda** (made with `lambda { }` or `->() { }`) behaves like a small method: `return` inside it ends the lambda and hands the value back to `call`. A **regular proc** (made with `proc { }`, `Proc.new { }` or by capturing a block) behaves like a block: `return` inside it ends the **method in which the proc was written**, the *enclosing method*. That rule is what makes this idiom work: ```ruby def first_blank(values) values.each { |v| return v if v.strip.empty? } :none end ``` The block's `return` leaves `first_blank` from inside `each`. It relies on `first_blank` still running when the block executes. ## Orphaned procs and LocalJumpError Now store the same kind of code for later. A form library builds its rules in a class method and keeps them: - `presence_rule` creates `proc { |v| return false if v.nil?; true }` and returns it. - The proc outlives `presence_rule`; the Proc documentation calls such an object an **orphaned proc**. - When the form later calls the rule with `"ann"`, no `return` executes and the result is `true`. - When it calls the rule with `nil`, `return` tries to leave `presence_rule`, which finished long ago. Ruby raises **`LocalJumpError`** with the message `unexpected return`. The exception carries two readers that help in a log or a debugger: | Reader | Value here | Meaning | |---|---|---| | `LocalJumpError#reason` | `:return` | which jump failed (`:break` for a break) | | `LocalJumpError#exit_value` | `false` | the value the jump tried to carry | The bug is path-dependent. Specs that only feed valid values never run the `return`, so it tends to surface in production on the first empty field. ## break has the same trap, with a different target `break` in a block ends the **method the block was given to** and makes the value that method's result: `[1, 2, 3].each { |n| break n if n > 1 }` returns `2`. Wrap the same code in `proc` first and the block now belongs to the `proc` call, which has already finished: | Code | Result | |---|---| | `[1, 2, 3].each { \|n\| break n if n > 1 }` | `2` | | `[1, 2, 3].each(&->(n) { break n if n > 1 })` | `[1, 2, 3]`: `break` only ends the lambda, `each` carries on | | `[1, 2, 3].each(&proc { \|n\| break n if n > 1 })` | `LocalJumpError`: `break from proc-closure` | So a proc can be orphaned for `break` while still valid for `return`: a block captured by one method and called later inside the method it was written in can still `return` from that method, but its `break` fails because the method it was given to has finished. **Lambdas cannot be orphaned** at all, because both keywords end the lambda itself. ## Fixing the validation rules 1. **Build stored callables as lambdas**: `->(v) { return false if v.nil?; true }`. The `return` is local, and the lambda's strict arity also catches a library calling the rule with the wrong number of arguments. 2. **Or avoid the jump**: `proc { |v| next false if v.nil?; true }` or simply `proc { |v| !v.nil? }`. `next` ends only the current invocation. 3. **Do not rescue `LocalJumpError` around rule calls.** It signals a structural bug in how the callable was written, not a runtime condition to recover from. 4. **Test the failing branch**: a spec that calls every rule with an invalid value triggers the jump in CI rather than in production. ## Diagnosing it in a running application - A backtrace that ends inside a stored block with `unexpected return` or `break from proc-closure` is almost always an orphaned regular proc. - Check `rule.lambda?` on the offending object; `false` confirms the diagnosis. - Look for where the proc was created, not where it was called. The creating method is the one the `return` is trying to leave. - Watch for code that converts a captured `&block` into a stored handler (callbacks, hooks, rule registries). Blocks written by callers are regular procs, so a caller's `return` inside one will blow up once the registering method has returned.
- The same proc { return } works when called inside the method that created it. Why?Because `return` in a regular proc targets its enclosing method, and that method's frame is still on the stack, so the jump is valid and the method returns early. The proc only becomes orphaned for `return` once the creating method has finished.
- Why does [1, 2, 3].each(&proc { |n| break n }) raise although each is still running?`break` targets the method the block was attached to, and here the block was attached to the call to `proc`, which returned before `each` started. The proc is orphaned for `break` even though its enclosing method is alive, so Ruby raises `LocalJumpError` with `break from proc-closure`.
- Why is rescuing LocalJumpError around rule calls a poor fix?It hides a structural bug: the rule cannot express its result, and every caller must know to rescue. Rewriting the callable as a lambda or with `next` removes the jump, so the rule returns its value normally and callers need no special handling.
A regular proc's return is a note saying 'end the meeting' written during one particular meeting. Read aloud while that meeting runs, it ends it; read out next week, the meeting is over and the instruction fails. A lambda's return only means 'I am done speaking'.
saying these in an interview costs you the question
- return inside a stored proc just returns from the proc
- LocalJumpError means the proc was called with the wrong arguments
- A lambda's return can also raise LocalJumpError once its method has finished
- Wrapping rule calls in rescue LocalJumpError is the right fix
- break inside any block always stops the iterator that yields to it