9

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?
missingfaktor
  • 90,905
  • 62
  • 285
  • 365
  • Application is not suposed to handle fatal error, as it's at JVM level – cchantep Apr 11 '18 at 12:07
  • If you think catching `Throwable` is bad because it encompasses `OutOfMemoryError` and other fatal errors, and you know that `NonFatal` filters out these fatal errors, then why would you think `NonFatal` is still bad? – Jasper-M Apr 11 '18 at 12:49
  • 1
    I think it depends on what you mean by "catch". Normally it's meant to mean a `catch` block that handles/swallows the exception. – Ionuț G. Stan Apr 11 '18 at 12:55
  • @Jasper-M, before rethrowing though, it does "catch" it, and my question is whether doing that creates problems. – missingfaktor Apr 11 '18 at 13:25
  • @IonuțG.Stan, I see. – missingfaktor Apr 11 '18 at 13:26
  • 1
    @missingfaktor I think you're right. It doesn't look like catching, but it actually does catch it and do something at the bytecode level, which is the filtering. – Ionuț G. Stan Apr 11 '18 at 13:28
  • 3
    Note that catching `Throwable` and doing something before rethrowing it, is what always happens when you use `try { … } finally {…}` or `try( … ) { … }`. Likewise, it always happens when you use `synchronized(…) { … }` – Holger Apr 11 '18 at 15:59

3 Answers3

13

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…

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Upvoted for the thorough research backed by code samples. – Ionuț G. Stan Apr 12 '18 at 10:14
  • @IonuțG.Stan doesn't answer the question though — which is indeed pretty normal on StackOverflow – Alexandru Nedelcu Apr 12 '18 at 12:12
  • 1
    @AlexandruNedelcu lacking any authoritative reference that it's okay to catch and re-throw `Throwable` with just a little bit of code executed in between the two actions, this answer provides at least some evidence that it's deemed ok to do that. Your answer says it's fine, too, but you don't provide evidence. And the evidence he shows here is coming from the JVM creators, so I have to assume they kind of knew what they did. – Ionuț G. Stan Apr 12 '18 at 12:20
  • 1
    @AlexandruNedelcu did you read the last paragraph of the answer? It precisely answers the question. Whether it is ok to catch `Throwable` depends on *why* you’re doing it. – Holger Apr 12 '18 at 13:18
  • @Holger yes, the final paragraph doesn't address the question and also it's actually a big problem catching *any throwable* for executing finalizers, plus the fact that Java executes "finally" regardless of the throwable is a big gotcha — b/c when the process is out of memory, it will probably be unable to execute finalizers — and I've seen it in practice. Don't get me wrong, your answer is cool and very educational, thanks for it, I just have a very old pet peeve with SO answers that don't answer the question, many authors trying to read between the lines – Alexandru Nedelcu Apr 12 '18 at 14:09
  • 1
    @AlexandruNedelcu `finally` has nothing to do with `finalize()`. The actions within `finally` should be simple and not depend on additional resources, e.g. like releasing a lock or closing a resource. Since, as explained, it’s just a variant of a `catch` block, no additional resources are needed to execute them.As you said yourself “catching and re-throwing `Throwable` isn't a problem per”, but I’d restrict that to language features and carefully designed frameworks like the examples. But it’s important to understand that ignoring exceptions recorded by `Future`s is like catching `Throwable`… – Holger Apr 12 '18 at 14:21
  • @Holger n.b. I am not talking of `finalize()`, I did not even think about that, I am talking of the general notion of finalizers. Also note that closing a file socket is far from simple or inexpensive because it involves system calls, which trigger context switches at least, let alone ops like fsync or even the full TCP close handshake. – Alexandru Nedelcu Apr 12 '18 at 14:47
  • 1
    @AlexandruNedelcu in the context of Java, it’s misleading to call it “finalizer”. However, I see your point, but I think, you’d have a hard time deciding which throwable allows an attempt to close the socket and which doesn’t. In the context of futures, not catching an error would just kill the particular thread without initiating a shutdown at all. In fact, the pool owner didn’t have any chance to detect that it should shut down if the future didn’t catch and report… – Holger Apr 12 '18 at 15:25
8

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:

  • people have been abusing Throwable / Error
  • NonFatal is also looking for things like InterruptedException, which is another best practice that people aren't respecting

That 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).

Alexandru Nedelcu
  • 8,061
  • 2
  • 34
  • 39
  • 1
    If you accept that non-local returns exist in Scala, even though they're bad, don't you agree that they should never be caught by user code? – Jasper-M Apr 11 '18 at 12:50
  • 2
    @Jasper-M no, I don't agree; e.g. many async abstractions try to mimic JVM's call stack and try/catch/finally for resource handling — a "finally" statement for example will always get executed, no matter what `Throwable` was thrown and if you're building any abstraction that needs to do basically what `finally` does, then by not catching `ControlThrowable` that's a disaster waiting to happen. Because even though a thrown `ControlThrowable` represents a bug — (1) crashes aren't necessarily cheap, not on the JVM and (2) other libraries will catch it (e.g. Netty). – Alexandru Nedelcu Apr 11 '18 at 13:02
1

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 NonFatals 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

simpadjo
  • 3,947
  • 1
  • 13
  • 38