Is it okay that NonFatal catches Throwable?

missingfaktor :

As I understand it, the best practice in Java/JVM dictate that you should never catch Throwable directly, since it covers Error which happens to encompass things like OutOfMemoryError and KernelError. Some references here and here.

However in Scala standard library, there is an extractor NonFatal that is widely recommended (and widely used by popular libraries such as Akka) as a final handler (if you need one) in your catch blocks. This extractor, as suspected, happens to catch Throwable and rethrow it if it is one of the fatal errors. See the code here.

This can be further confirmed by some disassembled bytecode:

Disassembled output

Questions:

  1. Is the assumption I made in my first paragraph correct? Or am I incorrect in assuming it's not okay to catch Throwable?
  2. If that assumption is correct, could the behaviour of NonFatal lead to serious problems? If not, why not?
Holger :

Note that catching Throwable happens more often than you might be aware of. Some of these cases are tightly coupled with Java language features which may produce byte code very similar to the one you have shown.

First, since there is no pendent to finally on the bytecode level, it gets implemented by installing an exception handler for Throwable which will execute the code of the finally block before rethrowing the Throwable if the code flow reaches that point. You could do really bad things at this point:

try
{
    throw new OutOfMemoryError();
}
finally
{
    // highly discouraged, return from finally discards any throwable
    return;
}
Result:

Nothing

try
{
    throw new OutOfMemoryError();
}
finally
{
    // highly discouraged too, throwing in finally shadows any throwable
    throw new RuntimeException("has something happened?");
}
Result:
java.lang.RuntimeException: has something happened?
    at Throwables.example2(Throwables.java:45)
    at Throwables.main(Throwables.java:14)

But of course, there are legitimate use cases for finally, like doing resource cleanup. A related construct using a similar byte code pattern is synchronized, which will release the object monitor before re-throwing:

Object lock = new Object();
try
{
    synchronized(lock) {
        System.out.println("holding lock: "+Thread.holdsLock(lock));
        throw new OutOfMemoryError();
    }
}
catch(Throwable t) // just for demonstration
{
    System.out.println(t+" has been thrown, holding lock: "+Thread.holdsLock(lock));
}
Result:
holding lock: true
java.lang.OutOfMemoryError has been thrown, holding lock: false

The try-with-resource statement takes this even further; it might modify the pending throwable by recording subsequent suppressed exceptions thrown by the close() operation(s):

try(AutoCloseable c = () -> { throw new Exception("and closing failed too"); }) {
    throw new OutOfMemoryError();
}
Result:
java.lang.OutOfMemoryError
    at Throwables.example4(Throwables.java:64)
    at Throwables.main(Throwables.java:18)
    Suppressed: java.lang.Exception: and closing failed too
        at Throwables.lambda$example4$0(Throwables.java:63)
        at Throwables.example4(Throwables.java:65)
        ... 1 more

Further, when you submit a task to an ExecutorService, all throwables will be caught and recorded in the returned future:

ExecutorService es = Executors.newSingleThreadExecutor();
Future<Object> f = es.submit(() -> { throw new OutOfMemoryError(); });
try {
    f.get();
}
catch(ExecutionException ex) {
    System.out.println("caught and wrapped: "+ex.getCause());
}
finally { es.shutdown(); }
Result:
caught and wrapped: java.lang.OutOfMemoryError

In the case of the JRE provided executor services, the responsibility lies at the FutureTask which is the default RunnableFuture used internally. We can demonstrate the behavior directly:

FutureTask<Object> f = new FutureTask<>(() -> { throw new OutOfMemoryError(); });
f.run(); // see, it has been caught
try {
    f.get();
}
catch(ExecutionException ex) {
    System.out.println("caught and wrapped: "+ex.getCause());
}
Result:
caught and wrapped: java.lang.OutOfMemoryError

But CompletableFuture exhibits a similar behavior of catching all throwables.

// using Runnable::run as Executor means we're executing it directly in our thread
CompletableFuture<Void> cf = CompletableFuture.runAsync(
    () -> { throw new OutOfMemoryError(); }, Runnable::run);
System.out.println("if we reach this point, the throwable must have been caught");
cf.join();
Result:
if we reach this point, the throwable must have been caught
java.util.concurrent.CompletionException: java.lang.OutOfMemoryError
    at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:314)
    at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:319)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1739)
    at java.base/java.util.concurrent.CompletableFuture.asyncRunStage(CompletableFuture.java:1750)
    at java.base/java.util.concurrent.CompletableFuture.runAsync(CompletableFuture.java:1959)
    at Throwables.example7(Throwables.java:90)
    at Throwables.main(Throwables.java:24)
Caused by: java.lang.OutOfMemoryError
    at Throwables.lambda$example7$3(Throwables.java:91)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1736)
    ... 4 more

So the bottom line is, you should not focus on the technical detail of whether Throwable will be caught somewhere, but the semantic of the code. Is this used for ignoring exceptions (bad) or for trying to continue despite serious environmental errors have been reported (bad) or just for performing cleanup (good)? Most of the tools described above can be used for the good and the bad…

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=443755&siteId=1