Misc #19054
closed`else` in exception-handling context vs early return
Description
else in exception-handling context is rarely used (at least in codebase I saw), so we encountered it just recently:
def foo
puts "body"
return
rescue => e
p e
else
puts "else"
ensure
puts "ensure"
end
foo # prints "body", then "ensure"
[1].each do
puts "body"
next
rescue => e
p e
else
puts "else"
ensure
puts "ensure"
end
# also prints "body" then "ensure"
E.g. else is ignored in both cases. Intuitively, I would expect that if no exception is raised in block, else is performed always—like ensure is performed always, exception or not, early return or not.
I found only a very old discussion of this behavior in #4473 (it was broken accidentally on the road to 1.9.2, but then fixed back), but it doesn't explain the reason for it.
Can somebody provide an insight on this decision, and whether it is justified at all?.. At least, it should be documented somewhere, exception handling docs doesn't mention this quirk.
Updated by jeremyevans0 (Jeremy Evans) about 3 years ago
I think the existing behavior is expected. else with rescue operates similar to else with if. In pseudocode:
begin
if e = exception_raised_by{puts "body"; return}
p e
else
puts "else"
end
ensure
puts "ensure"
end
I agree with you that the documentation could be improved, though it kind of hints at the current behavior:
- (Regarding
else): "You may also run some code when an exception is not raised" - "To always run some code whether an exception was raised or not, use ensure:"
It would be useful to document that else is not called on early exits or exceptions, and how to use ensure to run code on all non-exception scenarios (by using rescue => local_var and if local_var).
Updated by zverok (Victor Shepelev) about 3 years ago
Well, my pseudo-code-expressed intuition can be rather expressed like this:
begin
# whatever happens here, is covered by rescue/else/ensure block
e = exception_raised_by{puts "body"; return}
ensure
if e
# we go here if there was an exception
p e
else
# we go here if there was none
puts "else"
end
# we go here in any case
puts "ensure"
end
It is implied by else and rescue being on the same level as ensure, making me think there are 3 blocks of equal priority
- one that performs always (
ensure) - one that performs if there was exception ([one of the]
rescue) - one that performs if there was no exception (
else)
If we'll imagine more realistic code, there can be, like, 30 lines of methods body, and overall structure on reading looking like this:
def my_method
# a LOT can go here,
# ...but while reading
# ...to the very end
# ...you can always rely
# ...on the fact that
# even if THIS last line is not performed due to some reason,
rescue
# this WILL perform if any exception happens, however deep it was
else
# this WILL perform if no exception happened, no matter what
ensure
# this WILL perform no matter what, period
end
Again, for me it somewhat theoretical, but I can imagine good uses for else like, at the very least, log.debug 'success'. With current behavior, it seems completely redundant feature, because imagine this:
def my_method
return :early
puts "(1) printed at the end in normal, no early return-flow"
:regular
rescue
# ...
else
puts "I would hope this will print for both :early, and :regular, but it behaves JUST like (1)"
end
...e.g. NOTHING is achieved by else that can't be achieved by putting exactly the same code at the end of the body.
Updated by jeremyevans0 (Jeremy Evans) about 3 years ago
zverok (Victor Shepelev) wrote in #note-2:
...e.g. NOTHING is achieved by
elsethat can't be achieved by putting exactly the same code at the end of the body.
Incorrect. The primary reason for else with rescue is that code in else that raises exceptions does not call into the rescue blocks (though it is still handled by ensure). Code that the end of the body obviously would.
In any case, I would guess there is no chance of changing the behavior at this point. It would break way too much code.
Updated by jeremyevans0 (Jeremy Evans) about 2 years ago
- Status changed from Open to Closed