For the given sample, having two catch blocks is really redundant, yes. It could easily handled in one single catch block for IOException.
But let's assume we have a method like this:
public final Optional<Status> doSomething( final String... arguments )
throws IOException, SQLException, PatternSyntaxException
{
…
}
We don't care what the method is really doing, we just assume that it will not check the arguments for null – meaning we can see a NullPointerException under some circumstances.
Let's use that method now, with the error handling that you suggest:
try
{
final var status = doSomething( arguments ).get();
}
catch( final Exception e ) // Not good!
{
handleExpectedException( e );
}
Looks perfectly ok – but handleExcpectedException() may be called with some 'unexpected' exceptions, too: with a NullPointerException or with a NoSuchElementException.
Ok, second attempt:
try
{
final var status = doSomething( arguments ).get();
}
catch( final IOException e )
{
handleExpectedException( e );
}
catch( final SQLException e )
{
handleExpectedException( e );
}
catch( final PatternSyntaxException e )
{
handleExpectedException( e );
}
catch( final NullPointerException e )
{
handleUnexpectedException( e );
}
catch( final NoSuchElementException e )
{
handleUnexpectedException( e );
}
Confessed, that's a lot of lines, for nearly nothing. Therefore Java 5 (or Java 6?) introduced a way to combine the catch for multiple exceptions that are handled the same way:
try
{
final var status = doSomething( arguments ).get();
}
catch( final IOException | SQLException | PatternSyntaxException e )
{
handleExpectedException( e );
}
catch( final NullPointerException | NoSuchElementException e )
{
handleUnexpectedException( e );
}
But why do we want to list the exceptions at all? Why not just having two categories and handle them like this:
try
{
final var status = doSomething( arguments ).get();
}
catch( final RuntimeException e )
{
handleUnexpectedException( e );
}
catch( final Exception e )
{
handleExpectedException( e );
}
That's because we want that unexpected exceptions will bubble up when they occur! We want (at least, I want) that a program, that behaves completely unexpected, will fail fast! And loud!
The throws clause in the signature of doSomething() tells us, that it may throw three different types of exceptions (and usually, the respective documentation will tell us, why). So we can prepare our code to handle them. Just log them and terminate is the easiest way, but perhaps we know how to recover from the error condition and can retry, or we have an alternative processing, or whatever.
But the NullPointerException either indicates that there is a programming bug in the code of doSomething(), or we have not read the documentation properly and fed a null into it. Both is nothing that can be fixed during runtime.
And the NoSuchElementException is clearly caused by a bug in our code: doSomething() returns an instance of Optional, and that can be empty; calling Optional::get on the result without checking it is definitely a bug! And I want to see this bug soon, in order to be able to fix it soon!
A catch block for Exception may swallow these exceptions and they will never surface – and I will never find the reason why my program returns sometimes unexpected results …
So the recommendation is to catch only those exceptions that are expected at this level (either they are declared with a throws clause or described in the documentation) and that could be handled in the current catch block.
To 'handle' an exception means that the system is brought back into a stable state!
Logging a FileNotFoundException in the catch block and then continuing after that catch block like the (non-existing!) file was found and opened is calling for desaster!
If you just catch an exception locally because you want to enrich the error message, you should not log it; just wrap it and re-throw:
…
catch( final FileNotFoundException e )
{
final var message = "MatrixReport output file %s could not be found!".formatted( e.getMessage() );
throw new MyApplicationError( message, e );
}
So finally, when I would use doSomething() (without fixing the already mentioned bugs), my code may look like this (assuming that the FileNotFoundException is documented):
try
{
final var status = doSomething( arguments ).get();
}
catch( final FileNotFoundException e )
{
final var message = "MatrixReport output file %s could not be found!".formatted( e.getMessage() );
throw new MyApplicationError( message, e );
}
catch( final IOException | SQLException | PatternSyntaxException e )
{
handleExpectedException( e );
}
Ok, we handle only exceptions that we can handle, and let the rest bubbling up … but who will now take care of these exceptions?
When this happens in the main thread, the exception that terminates it will be printed to stdout. Great! And what about the other threads?
Usually, these will die silently …
So you quite often see the recommendations to have a catch block for RuntimeException in a thread's run() method, like this:
public final void run()
{
try
{
// Do this thread's work …
}
catch( final RuntimeException e )
{
log.error( e );
}
}
But this can be achieved easier: you can set an UncaughtExceptionHandler instance to the Thread! See the JavaDoc. You can do this even implicitly by assigning the thread to a specific ThreadGroup that provides an UncaughtExceptionHandler (you have to implement your own class that extends ThreadGroup for that), or you have a ThreadFactory that does the assigning for you …
Oh, before I forget: I introduced handleExpectedException() for a reason, instead of simply using log.error(): if you do not fix the status that is indicated by the exception, you may not continue the regular flow of the code! Logging an exception is not handling it!
This means handleException() will not return regularly in most cases; perhaps it throws an instance of MyApplicationError, too, or it will call System::exit (really nasty when you are in a web application … don't do it!) or whatever is appropriate …