In Ruby, what happens when an ensure clause runs an explicit return while an exception is propagating out of the method?
answer
- ensure value normally ignored
- explicit return overrides everything
- the in-flight exception vanishes
- raise in ensure replaces it
- Lint/EnsureReturn
basics
~20 sThe explicit return wins: the method returns the ensure's value and the propagating exception is silently discarded, as if rescued. It also overrides a normal return value. RuboCop's Lint/EnsureReturn flags it; keep ensure for cleanup only.
solid answer
~40 sNormally the value of `ensure` is ignored and an exception that no rescue matched keeps propagating after `ensure` finishes. An explicit `return` inside `ensure` changes both: the method returns that value, and the in-flight exception is dropped with no trace, so a failed charge or write looks like success. It also overrides a `return` from the method body. `break` or `next` in an `ensure` inside a block discard the exception the same way. A related trap is an `ensure` that raises: the new exception replaces the original, which survives only as the new one's `cause`. RuboCop's `Lint/EnsureReturn`, enabled by default, reports `return` in `ensure`. Keep `ensure` to cleanup that cannot fail, and guard risky cleanup with its own small `rescue`.
code
ruby · 9 linesdef save_export(rows)
raise IOError, "disk full" if rows.empty?
:saved
ensure
puts "closing export file"
return :done # swallows the IOError; Lint/EnsureReturn
end
p save_export([]) # prints closing export file, then :done; no errorgo deeper
Know that ensure always runs and that its value is normally ignored.
Explain that an explicit return in ensure overrides the result and discards an in-flight exception, and why RuboCop flags it.
Show how cleanup failures mask root causes, and write ensure blocks that guard risky cleanup with their own rescue and never jump out.
Treat silent exception loss as a reliability defect class: lint rules on by default, review checklists for ensure blocks, and reporters that show cause chains.
## What ensure is for An **`ensure`** clause runs whenever control leaves its `begin` block, method or `do...end` block: after normal completion, after a rescue, during an early `return`, and while an unrescued exception is on its way out. Two rules make it safe: - its **value is discarded**, so it cannot change the result by accident; - after it finishes, **whatever was happening continues**: the return value is returned, or the exception keeps propagating. An explicit jump inside `ensure` breaks the second rule. ## return inside ensure When `ensure` executes `return`, the method returns immediately with that value. Whatever was in flight is abandoned: | In flight when ensure ran | Effect of `return :x` in ensure | |---|---| | normal completion with value `1` | method returns `:x` instead of `1` | | `return 1` from the body | method returns `:x` instead of `1` | | an unrescued `RuntimeError` | the exception is discarded; method returns `:x` | The last row is the dangerous one. Nothing is logged, no handler further up ever sees the error, and the caller receives a normal value. RuboCop's documentation for **`Lint/EnsureReturn`** describes it exactly: the return takes precedence over any exception being raised, and the exception is thrown away as if it were rescued. The same applies to **`break`** and **`next`** inside an `ensure` that sits in a block: the jump wins and the exception is lost. `break` also replaces the value of the method the block was passed to. ## raise inside ensure Cleanup code can fail too. If `ensure` raises while another exception is propagating: 1. The **new exception replaces** the original as the one that propagates. 2. The original is attached as the new exception's **`cause`**, because Ruby records the exception being handled at the time of the raise. 3. Error reporters that show only the top-level exception now report the cleanup failure, not the real problem. In a file-export job, a `close` that fails because the disk filled up would hide the original write error that filled it. ## Writing ensure safely - **Only cleanup.** Close, unlock, delete temp files, restore state. Compute results in the body or `else`. - **No `return`, `break`, `next` or `throw`.** If you want to swallow an exception, write an explicit `rescue` that says so. - **Guard cleanup that can raise.** Wrap it in its own `begin ... rescue ... end`, log the cleanup failure and let the original exception continue. - **Make cleanup idempotent**, so checking state (`started?`, `closed?`) before closing avoids a second exception. - **Keep the linters on.** `Lint/EnsureReturn` is enabled by default in RuboCop and Standard keeps it on. ## Spotting it in review A method that ends with `ensure` and a `return self` or `return result` for convenience is the usual culprit: it was written so the method always returns the receiver, and nobody noticed that it also turns every exception into success. Move the value to the body's last line, before `ensure`, and the behaviour becomes correct with the same result on the success path. ## Why the language allows it `return` in `ensure` is not a syntax error because there are rare legitimate uses, such as a method that must return a sentinel whatever happens and deliberately discards failures. Even then, an explicit `rescue` that says which errors are being discarded is clearer, and it can log them. Ruby leaves the choice to the programmer; RuboCop encodes the community's verdict that the silent version is almost always a mistake. When you do find the pattern in an existing codebase, check the call sites before fixing it: some callers may have come to rely on the method never raising, and removing the `return` will surface failures that were hidden for a long time.
- Does a return in ensure also override a return from the method body?Yes. The body's `return 1` starts leaving the method, `ensure` runs, and its explicit `return 2` replaces the value, so the caller gets `2`. Without the explicit return, `ensure`'s last value would be discarded and the caller would get `1`.
- If ensure raises while another exception is propagating, is the original lost?It is no longer the exception that propagates, so handlers and reporters see the cleanup error. Ruby does keep the original as the new exception's `cause`, so a reporter that walks the cause chain can still show it. Guarding the cleanup with its own rescue avoids the replacement entirely.
saying these in an interview costs you the question
- A return in ensure only changes the value when no exception occurred
- The exception is re-raised after ensure's return completes
- ensure's last expression always becomes the method's result
- An exception raised in ensure is added to the original one
- return in ensure is a good way to guarantee a return value