Proposal: `finally` block exception suppression

some-java-user-99206970363698485155 at vodafonemail.de some-java-user-99206970363698485155 at vodafonemail.de
Sun Jul 10 19:35:56 UTC 2022


This is not a formal JEP because I am not a OpenJDK member, and also because I would
still like to get some feedback on this proposal. But it would be great if you could
create a JEP if you consider this to be a useful proposal.
This proposal originated from https://mail.openjdk.org/pipermail/core-libs-dev/2022-April/089016.html
Thanks a lot already for David Holmes' initial feedback.


# Summary

Add automatic exception suppression when exceptions are thrown by `finally` blocks
to preserve exceptions which occurred in `try` or `catch` blocks.


# Description

If exception E1 causes a `finally` block to be executed and the `finally` block is exited due
to a thrown exception E2 (E2 may be the same as E1), then E1 is added as suppressed exception
to E2.

E1 can be an exception which occurred in the `try` block, or any of the `catch` blocks.

This requires only changes in the compiler, the language reference and the tests.
The language grammar is not affected.

The behavior would be the same as the one shown by this code, where every line prefixed
with # would be the code generated by the compiler:
```
# Throwable e1 = null;
try {
    # try {
        ... // content of try block
    # } catch (Throwable t) {
    #    e1 = t;
    #    throw e1;
    # }
} catch (...) {
    # e1 = null;
    # try {
        ... // content of catch block
    # } catch (Throwable t) {
    #    e1 = t;
    #    throw e1;
    # }
} finally {
    # try {
        ... // content of finally block
    # } catch (Throwable e2) {
    #    if (e1 != null) {
    #        e2.addSuppressed(e1);
    #    }
    #    throw e2;
    # }
}
```


# Rationale

Despite the existence of the try-with-resources statement which properly handles exception
suppression, there are still situations where a regular try-(catch-)finally statement has
to be used for clean-up actions, for example to call Lock.unlock().
Usually the `try` block contains the main business logic and the `finally` block contains
code for cleaning up resources or restoring a previous state. Therefore an exception which
occurs in the `try` block is usually important to debug any application failures.

The problem is that if the `finally` block is exited because an exception is thrown, then
the original exception from the `try` block is lost; it is not even added as suppressed
exception. A very simplified example for this is the following:
```
try {
    throw new Exception("try");
} finally {
    throw new Exception("finally");
}
```
(in more realistic code, instead of the `throw` statements there would be method calls which
might throw exceptions)

Here the original exception `new Exception("try")` is completely lost. This can make
debugging or reconstructing failures extremely difficult.

This issue was also mentioned in the original try-with-resources / automatic resource
management proposal (https://mail.openjdk.org/pipermail/coin-dev/2009-February/000011.html).
However, back then it appears no proposal was made to adjust the behavior of `finally`
blocks, most likely because suppressed exceptions was only an optional feature initially.
(Note that I have not looked at all mails from the coin-dev mailing list, so I might have
missed a discussion about this.)


# Risks / considerations

The proposed change would affect all source code which is recompiled; already compiled
code would not be affected.

- Noticeable behavior change
  The behavior change described in this proposal would be noticeable to applications
  which access the suppressed exceptions explicitly or implicitly (e.g. by printing
  the stack trace). However, it is unlikely that applications expect certain suppressed
  exceptions, especially since using expressions for regular control flow is discouraged.

- Memory usage impact
  The proposed change could have a performance impact because it would keep the exception
  thrown by the `try` or one of the `catch` blocks alive for a longer time. However,
  due to the improved debuggability this provides, that might be acceptable.

- Execution time impact
  The proposed change might have a negative effect on the execution time of `try` statements
  with `finally` blocks, possibly also negatively affecting the analysis performed by the
  JIT compiler.
  javac could however omit the exception suppression if the statements in the `finally`
  block are guaranteed to never throw exceptions.

- `addSuppressed` call causing StackOverflowError and OutOfMemoryError
  The call to `addSuppressed` in the `finally` block could trigger a StackOverflowError
  or an OutOfMemoryError. This would cause all previous exceptions to be lost.
  The StackOverflowError could possibly be avoided by using @ReservedStackAccess
  (introduced by JEP 270)?

- Self suppression
  Currently the proposal allows E1 and E2 to be the same exception instance. However,
  calling `e.addSuppressed(e)` is forbidden and will cause an IllegalArgumentException
  to be thrown. Such a situation could occur when the thrown exception had been stored
  in a field and is then rethrown by the clean-up actions.
  Not checking for self-suppression and accepting the risk of an IllegalArgumentException
  would match the current behavior of the try-with-resources statement.
  It should be discussed if that behavior is desired. At least the IllegalArgumentException
  is currently constructed with the self-suppressed exception as cause, so it is not "lost".


# Related issues / code segments

- https://bugs.openjdk.org/browse/JDK-4988583
  Seems to be the same as this proposal, but was (erroneously?) closed when suppressed
  exceptions were added, even though suppressed exceptions on their own do not solve
  this issue.
- https://bugs.openjdk.org/browse/JDK-5108147
  Suggests the addition of a `finally (Throwable) { ... }`. With such a construct
  it would at least be easier to write code which manually suppress exceptions
  which occur in the `finally` block.
- https://github.com/openjdk/jdk/blob/d53b02eb9fceb6d170e0ea8613c2a064a7175892/src/java.base/share/classes/java/util/stream/Streams.java#L837-L887
  Acts very similar to what is described in this proposal, except that there the
  exception from the `try` block is considered the "main" exception, and exceptions
  from the `finally` block are added as suppressed ones.
- https://github.com/openjdk/jdk/blob/d53b02eb9fceb6d170e0ea8613c2a064a7175892/src/java.net.http/share/classes/jdk/internal/net/http/websocket/WebSocketImpl.java#L737-L763
  Acts similar except that the resulting exception is not rethrown but logged.
- https://bugs.openjdk.org/browse/JDK-7172206
  Affected by exactly the flaw outlined in the rationale of this proposal.
- https://bugs.openjdk.org/browse/JDK-8267532
  Optimization problems with try-finally.


# Further notes

These notes are not part of the proposal itself, but highlight related issues.
Some of them had been described in https://mail.openjdk.org/pipermail/core-libs-dev/2022-April/088435.html
and the replies to it.

- Which expression should be rethrown, which should be suppressed?
  Currently the proposal suggests to keep the behavior of treating the exception from
  the `finally` block as "main" exception, and adding the exception from the `try` or
  `catch` block as suppressed exception.
  However, as also highlighted during the discussion for the try-with-resources statement,
  the exception from the `try` or `catch` block is most likely the more important exception,
  whereas the exception from the `finally` block is not as important. In fact the exception
  from the `finally` block might have only occurred due to the original failure in the
  `try` block which had left the application in an inconsistent state.
  The try-with-resources statement behaves like this and treats the exceptions from the
  `close()` invocation as suppressed. Maybe it would be worth to consider whether the
  behavior of `finally` should be changed to match this, but that might cause backward
  compatibility issue.

- Other control flow statements in `finally`
  The `finally` clause has similar issues when it is exited with other control flow
  statements, such as `return`, `break` or `continue`. For all of these the original
  exception would be lost, even with the changes of this proposal. Additionally, these
  statements can also "overrule" other control flow statements in the body of `try` or
  `catch`:
  ```
  while (...) {
      try {
          // Has no effect
          return 0;
      } finally {
          continue;
      }
  }
  ```
  
- Further enhancements for exception handling
  There are other exception handling situations which currently require rather verbose
  code, most notably:
    - Given an existing exception E1 (for example from a `catch` clause) and an action
      which has to be executed but may throw an exception:
      Perform the action and add any exception E2 (of specific type?) thrown by that
      action as suppressed exception to E1 and throw E1.
      (Possibly also a variant where E1 may be null in which case E2 is thrown.)

    - Given multiple actions which all have to be executed but may throw exceptions:
      Execute all of them, record the first occurred exception E1 and add any subsequent
      exception E2 as suppressed one to E1. Then at the end throw E1.

  See also https://bugs.openjdk.org/browse/JDK-8073382.
  However, such changes would require language changes or introduction of new utility
  classes and methods.



More information about the amber-dev mailing list