On 21/04/2022 7:30 am, some-java-user-99206970363698485...@vodafonemail.de wrote:
Thanks a lot for your feedback! My original message was a bit vague in parts, 
sorry for that.

The proposal consists of the following:
1. Forbidding usage of `break`, `continue`, `yield` and `return` in `finally` 
blocks
2. Adding exceptions as suppressed exceptions:
    If exception E1 led to execution of the `finally` block and the block is 
left due to
    another exception E2, then either E1 or E2 should be thrown with the other 
one added
    as suppressed exception. For consistency with try-with-resources and 
because E1 is
    most likely more relevant ideally E1 would be thrown and E2 should be 
suppressed. But
    if you think the impact on backward compatibility would be too large, then 
E2 should
    be thrown (the current behavior), but E1 should at least be added as 
suppressed exception.

The following replies to your comments hopefully make the rationale for these 
proposed
changes clearer.


The behaviour of try/catch/finally is not "obvious at all", you have to read 
what it means
and when you do that you clearly see what happens regarding exceptions.

For try-catch you see that the code catches some specific exception and then 
handles it in a
certain way, whether that is by rethrowing, logging or itentionally ignoring it.
The issue with `finally` is that in its current form, exceptions which occurred 
in the `try`
block might just silently disappear. Consider this simple example:
```
try {
     throw new RuntimeException();
} finally {
     return true;
}
```
Here it is not clear at all, unless you have read the JLS in detail, that the 
thrown exception
just vanishes. There is no explicit indication that the `finally` has any 
effect on the
thrown exception. Of course this is a contrived example, but consider the same 
situation where
the `try` block calls a method which might throw an exception (or the general 
case that a
VirtualMachineError occurs), and that the `return` (or any of the other 
affected statements)
is not the only statement in the `finally` block.

Similar confusing code can be written where the `try` or `catch` block returns 
a value
(or continues or breaks loop iteration), but the `finally` block overwrites 
that action:
```
try {
     return 1;
} finally {
     return 2;
}
```
Again, contrived, but consider the same code with additional statements in the 
`try` and
`finally` blocks. This breaks fundamental assumptions one has about the 
behavior of the
statements, such as `return`.

I still maintain this is simply a matter of user education. You have no business using a finally block if you have never bothered to even learn how it works. Sorry no sympathy from me on that one.

Are there any plans to forbid usage of `break`, `continue`, `yield` and 
`return` in
`finally` blocks?

Why would we do that? What would that gain?

It would prevent code such as the one shown above, where code, most likely 
unintentionally,
discards exceptions. Now also consider that inside the `try` block a 
VirtualMachineError
(or a subclass of it) may occur which you really should not catch. Yet the code 
in the
`finally` block silently discards it without you ever noticing the error, 
before it occurs
later in another part of the application again and has possibly already 
corrupted the state
of the application.

Again this is primarily user education. And there are times when you definitely do want to use those statements and you would have to jump through hoops to get the same effect if they were banned.

But in terms of "are there any plans ..." the answer is a clear no as the compatibility issues would make this a non-starter.


Similarly for `throw` and any implicitly thrown exceptions, is there a plan to 
at least
add the original exception as suppressed one to the newly thrown?

That suggestion is not completely without merit, but nor is it a "slam-dunk" obvious thing to do. 
The semantic implications of the exceptions matter, and semantics come from the programmers intent. There's 
no reasonable way to automagically determine that when an exception is created that another exception (that 
led to the finally block) should be inserted as a "suppressed exception". That would actually be 
completely wrong to do in many circumstances you would instead need to act when the exception would terminate 
the finally block and then add the original exception as the "suppressed" exception.

To clarify my suggestion: If a `finally` block is entered due to an exception 
E1, and is
exited due to an exception E2 (regardless of whether explicitly thrown by a 
`throw` statement),
and E1 != E2, then both exceptions should be preserved, with one being added as 
suppressed
exception to the other one.

That is not unreasonable: E2 suppresses E1.


But really the programmer is in the best position to decide how exceptions need 
to be handled.

Except that in a `finally` block they don't have access to the exception which 
led to execution
of the `finally` block, unless they write verbose hacky code to first have a 
`catch (Throwable)`
which stores the caught exception in a local variable. To me it appears for 
many `finally`
blocks in existing code it was not actively decided how exception handling 
should be done,
but rather it was forgotten that the original exception might get discarded 
when the `finally`
throws a new exception; or that behavior was considered acceptable because 
currently Java does
not allow you to handle it in a better way (which is one of the main points of 
this proposal).

The programmer writing the finally block also wrote, or has access to the try block that allows the original exception, so they are still in the best position to determine how any given chunk of code should react to exceptions IMO.

Of course one could argue that the same applies to `catch` clauses whose body 
accidentally
causes an exception which discards the caught one. However the main difference 
is that

Yes exactly the same.

This point was mainly a safeguard in my proposal to avoid someone extending the 
scope of
this proposal to `catch` clauses as well, which then could cause this proposal 
as a whole
to be turned down. Therefore I am explicitly not arguing for any changes to 
`catch` clauses
exception handling / suppression here, because there it is at least somewhat 
more obvious
what is going on with the exceptions.

there, only a specific exception type is caught (and discarded), whereas for 
`finally`
exceptions of _any_ type are discarded. It could also be argued that adding 
suppressed

We have multi-catch now so that argument is somewhat weaker.

The point here was that you explicitly write the exception type, whether that 
is a single type
or a union type in case of a multi-catch, in constrast to how `finally` 
discards any exception
type without you explicitly naming the type.

exceptions decreases performance or causes memory leaks, but one the other hand
this behavior was explicitly included try-with-resources statements, most 
likely because the
value this adds was considered more important than any performance issues this
might cause.

try-with-resources added support for suppressed exceptions because the 
automatic closing of the resource could throw an exception, and that had to be 
factored in to the whole mechanism.

Yes, but I think the scenarios are exactly identical:
try-with-resources:
   1. Original exception E1 occurs
   2. Resource cleanup is done, here through implicit `close()`
   3. `close()` throws an exception E2
   4. E2 is added as suppressed to E1

Current `finally` block behavior:
   1. Original exception E1 occurs
   2. Resource cleanup is done, here manually in `finally` block
   3. `finally` block throws an exception E2
   4. !! E1 is silently lost !!

Why is it here acceptable to lose the original exception E1, even though the use
cases are exactly the same?

try-with-resources (twr) had to introduce suppressed exceptions to deal with the fact that the majority of close() operations throw a checked IOException, so twr had to deal with that. If trw had been implemented as a simple try/finally then the close() exception is the one that would be thrown. The designers of twr didn't want that because they decided that the exception from the try block was probably much more important to be seen in this context, than the IOException from the close(). So they introduced suppressed exceptions and defined twr such that the exception from the try block always gets rethrown by suppressing any exception from the finally/close(), and that exception from the close() is available through suppressed exception API.

So in the specific context of twr the original exception E1 was considered too important relative to the IOException (E2) from close() to allow it to be lost. So they worked around the behaviour of try/finally.

Given that we now have suppressed exceptions I agree there is merit in reusing it outside twr to allow E1 to not be lost when E2 occurs. But it would be suppression in the opposite sense to twr: the finally exception, E2, suppresses the try exception, E1.

I assume the reason why they behave differently is historic, because `finally` 
blocks
exist way longer than suppressed exceptions. However, that does in my opinion 
not
preclude updating it retroactively, especially when it is unlikely that the 
difference
will have a visible negative effect at runtime, but instead makes understanding
application failures easier.

As I said I agree there is merit to this aspect, but it is not a core-libs issue. This requires a language change, and supporting javac changes. I suggest taking up this one aspect on the amber-dev mailing list:

https://openjdk.java.net/projects/amber/

I don't know if extending the use of suppressed exceptions came up when twr (automatic resource management) was developed under Project Coin:

https://openjdk.java.net/projects/coin/

Cheers,
David


Kind regards


Reply via email to