Project

General

Profile

Actions

Misc #19054

closed

`else` in exception-handling context vs early return

Added by zverok (Victor Shepelev) over 1 year ago. Updated 6 months ago.

Status:
Closed
Assignee:
-
[ruby-core:110284]

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) over 1 year 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) over 1 year 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) over 1 year ago

zverok (Victor Shepelev) wrote in #note-2:

...e.g. NOTHING is achieved by else that 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.

Actions #4

Updated by jeremyevans0 (Jeremy Evans) 6 months ago

  • Status changed from Open to Closed
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0