Nested try-finally blocks, equivalence and guarantees in Java

109 Views Asked by At

Recently I was working on a "cleanup" method and I wanted to make sure that each statement is attempted to be executed. My first approach was something like this (where s#; are statements):

try {
    s1;
} finally {
    try {
        s2;
    } finally {
        s3;
    }
}

For simplicity I have omitted the exception handling part here (the catch blocks), where I store the first occurring and subsequent suppressed Throwables to rethrow later.

Then I noticed that there is another (maybe more correct?) way to do the same thing:

try {
    try {
        s1;
    } finally {
        s2;
    }
} finally {
    s3;
}

In my opinion the second approach feels more correct because as soon as s1; is attempted, we are already inside both try blocks, which guarantees that all finally blocks will be executed.

My question is: Is there actually a difference between those approaches in terms of guarantees that the JVM provides?

Also is it theoretically possible that an Error is thrown by the JVM in the first code example at // not a statement (see code below), such that the inner try-finally block isn't attemped anymore?

try {
    s1;
} finally {
    // not a statement
    try {
        s2;
    } finally {
        s3;
    }
}

According to 11.1.3. Asynchronous Exceptions, exceptions can be thrown basically anywhere. This is the main reason why I think that nesting the try-finally blocks is necessary as shown in the second code example. Or do I understand the JLS wrong and exceptions can only occur if there is also a statement?

2

There are 2 best solutions below

2
On

Add an s4 and an s5 into the mix and it becomes even clearer than it already should be that this is a hard to maintain mess that requires comments explaining why you've got this bizarro mix of nested try blocks.

Hence, I suggest you do this instead:

Throwable t = null;
try { s1(); } catch (Throwable e) {t = e;}
try { s2(); } catch (Throwable e) {t = e;}
try { s3(); } catch (Throwable e) {t = e;}
if (t != null) throw t;

You can fancy that up some and replace t = e; with a utility method (t = update(t, e);) which, if an exception has already occurred, will attach them as suppressed exceptions just like how try-finally does it.

Or, even nicer, add lambda support:

public final class Cleanup {
  public interface CleanupJob<T extends Throwable> {
    void cleanup() throws T;
  }

  @SafeVarargs
  public static <T extends Throwable> void cleanup(CleanupJob<? extends T>... jobs) {
    Throwable ex = null;
    for (var job : jobs) try {
      job.cleanup();
    } catch (Exception e) {
      if (ex == null) {
        ex = e;
      } else {
        ex.addSuppressed(e);
      }
    }

    if (ex != null) throw (T) ex;
  }
}

To use:

import static CleanupRunner.cleanup;

...

cleanup(
() -> s1(),
() -> s2(),
() -> s3(),
() -> s4());

// or even

cleanup(
  this::s1,
  ref::s2,
  SomeClass::staticS3
);

Also is it theoretically possible that an Error is thrown by the JVM in the first code example at // not a statement (see code below), such that the inner try block isn't attemped anymore?

It is not.

2
On

"... I wanted to make sure that each statement is attempted to be executed (either all or none at all). ..."

To preface, with the provided examples, all of the statements will be executed.
There would be no, "... or none at all".

Utilize a boolean, here is an example.

boolean exit = false;
try {
        
} catch (Throwable t) {
    exit = true;
} finally {
    if (!exit) {
        try {

        } catch (Throwable t) {
            exit = true;
        } finally {
        
        }
    }
}

Typically, a finally code-block is used to simply apply a final procedure.
Which, in some cases is actually necessary, e.g., closing a Closeable object.
And realistically, this is why the AutoCloseable interface, and the try-with-resources syntax, was implemented.

int value;
try {
    value = Integer.parseInt("abc");
} finally {
    value = 123;
}

In other-words, while your code is permissible, and valid, you typically would not want to keep nesting the try code-blocks.
Simply re-evaluate your current design and control-flow.

"... Then I noticed that there is another (maybe more correct?) way to do the same thing ..."

Correct, this example will provide the same functionality.
And, I imagine the interpreter would compile a similar "bytecode", for each.

"... Is there actually a difference between those approaches in terms of guarantees that the JVM provides? ..."

The guarantees would be the same; the abstraction is the same.
Each finally block will be executed, regardless of whether a Throwable is thrown or not; unlike the catch block.

"... is it theoretically possible that an Error is thrown by the JVM in the first code example ... such that the inner try block isn't attemped anymore? ..."

Similarly, if an Error is thrown at that line, any subsequent code would not be executed.
The program will simply halt and exit.
You would need to place that code within a try-catch block as well.

try {

} finally {
    try {
        throw new Error();
    } finally {

    }
}