Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java - detect whether there is an exception in progress during `finally` block

I am considering this from the Java Language Specification:

If the catch block completes abruptly for reason R, then the finally block is executed. Then there is a choice:

  • If the finally block completes normally, then the try statement completes abruptly for reason R.

  • If the finally block completes abruptly for reason S, then the try statement completes abruptly for reason S (and reason R is discarded).

I have a block as follows:

try {
  .. do stuff that might throw RuntimeException ...
} finally {
  try {
    .. finally block stuff that might throw RuntimeException ...
  } catch {
    // what to do here???
  }
}

Ideally, I would want any RuntimeException thrown in the finally block to escape, only if it would not cause a RuntimeException thrown in the main try block to be discarded.

Is there any way in Java for me to know whether the block that is associated with a finally block completed normally or not?

I'm guessing I could just set a boolean as the very last statement of the main try block (e.g., completedNormally = true. Is that the best way, or is there something better / more standard?

like image 475
Matthew McPeak Avatar asked Dec 16 '19 16:12

Matthew McPeak


2 Answers

I believe the key is to not lose the original cause if any.

If we look at how try-with-resources behave:

private static class SomeAutoCloseableThing implements AutoCloseable {

    @Override
    public void close() {
        throw new IllegalStateException("closing failed");
    }

}

public static void main(String[] args) {
    try (SomeAutoCloseableThing thing = new SomeAutoCloseableThing()) {
        throw new IllegalStateException("running failed");
    }
}

We end up with:

Exception in thread "main" java.lang.IllegalStateException: running failed
    at Main.main(Main.java:16)
    Suppressed: java.lang.IllegalStateException: closing failed
        at Main$SomeAutoCloseableThing.close(Main.java:9)
        at Main.main(Main.java:17)

This stack trace is great as we see both exceptions, i.e. we don't lose the running failed one.


Implementing this without try-with-resources, the wrong way:

public static void main(String[] args) {
    SomeAutoCloseableThing thing = new SomeAutoCloseableThing();
    try {
        throw new IllegalStateException("running failed");
    } finally {
        thing.close();
    }
}

We end up with:

Exception in thread "main" java.lang.IllegalStateException: closing failed
    at Main$SomeAutoCloseableThing.close(Main.java:9)
    at Main.main(Main.java:19)

We don't know that running failed occurred too as we broke the control flow, that's quite bad if you need to debug such a case.


Implementing this without try-with-resources, the right way (in my opinion), is to "log and forget" the exception that occurred in the finally block:

public static void main(String[] args) {
    SomeAutoCloseableThing thing = new SomeAutoCloseableThing();
    try {
        throw new IllegalStateException("running failed");
    } finally {
        try {
            thing.close();
        } catch (Exception e) {
            LoggerFactory.getLogger(Main.class).error("An error occurred while closing SomeAutoCloseableThing", e);
        }
    }
}

We end up with:

17:10:20.030 [main] ERROR Main - An error occurred while closing SomeAutoCloseableThing
java.lang.IllegalStateException: closing failed
    at Main$SomeAutoCloseableThing.close(Main.java:10) ~[classes/:?]
    at Main.main(Main.java:21) [classes/:?]
Exception in thread "main" java.lang.IllegalStateException: running failed
    at Main.main(Main.java:18)

Not as good as the try-with-resources approach, but at least we know what actually happened, nothing got lost.

like image 71
sp00m Avatar answered Sep 20 '22 03:09

sp00m


I assume your finally block is doing cleanup. A good way to accomplish such cleanup is to create a class that implements AutoCloseable, so your code can place it in a try-with-resources statement:

class DoStuff
implements AutoCloseable {
    public void doStuffThatMightThrowException() {
        // ...
    }

    @Override
    public void close() {
        // do cleanup
    }
}

(Notice that it does not need to be a public class. In fact, it probably shouldn’t be.)

The code in your example would then look like this:

try (DoStuff d = new DoStuff()) {
    d.doStuffThatMightThrowException();
}

As for what happens if an exception is thrown during the cleanup: it becomes a suppressed exception. It won’t show up in a stack trace, but you can access it if you really want to (which you probably won’t).

like image 22
VGR Avatar answered Sep 21 '22 03:09

VGR