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:
Questions:
Throwable
?NonFatal
lead to serious problems? If not, why not?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…
Catching throwable is not recommended because whatever processing that you're doing could delay the process rightfully crashing (in the event of an out of memory error) and then ending up in a zombie like state, with the garbage collector desperately trying to free up memory and freezing everything. So there are instances in which you need to give up on any active transactions you may have and crash as soon as possible.
However catching and re-throwing Throwable
isn't a problem per se if what you're doing is a simple filter. And NonFatal
is evaluating that Throwable
to see if it's a virtual machine error, or the thread being interrupted, etc, or in other words it's looking for the actual errors to watch out for.
As for why it is doing that:
Throwable
/ Error
NonFatal
is also looking for things like InterruptedException
, which is another best practice that people aren't respectingThat said Scala's NonFatal
isn't perfect. For example it is also re-throwing ControlThrowable
, which is a huge mistake (along with Scala's non-local returns).
If you catch an exception without rethrowing it further it means that you can guarantee that the program stays in correct state after the catch
block is finished.
From this point of view it doesn't make any sense to catch, say, an OutOfMemoryError
because if it has happened you can't trust you JVM anymore and can't soundly repair the state of your program in catch
block.
In Java it's recommended to catch at most Exception
, not Throwable
. Authors of NonFatal
construct have a bit different opinion about which exceptions are repairable and which are not.
In scala I prefer to catch NonFatal
s instead of Exceptions
but catching Exceptions as in Java is still valid.
But be prepared for surprises:
1) NonFatal
catches StackOverflowError
(it makes no sense from my point of view)
2) case NonFatal(ex) =>
is a scala code that has to be executed by JVM after the exception has already occurred. And the JVM can be already broken at this moment.
I faced once something like java.lang.NoClassDefFoundError
for NonFatal
in my logs but the real reason was StackOverflowError
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With