How and when to use Exceptions
This article is part of our Academy Course titled Advanced Java.
This course is designed to help you make the most effective use of Java. It discusses advanced topics, including object creation, concurrency, serialization, reflection and many more. It will guide you through your journey to Java mastery! Check it out here!
Table Of Contents
1. Introduction
Exceptions in Java are an important instrument to signal abnormal (or exceptional) conditions in the program flow which may prevent it to make a further progress. By nature, those exceptional conditions may be fatal (the program is not able to function anymore and should be terminated) or recoverable (the program may continue to run though some functionality may not be available).
In this section of the tutorial we are going to walk through the typical scenario of using exceptions in Java, discuss the checked and unchecked exceptions, and touch some corner cases and useful idioms.
2. Exceptions and when to use them
In a nutshell, exceptions are some kind of events (or signals) which occur during program execution and interrupt the regular execution flow. The idea which led to introduction of the exceptions was born as a replacement to the error codes and status checking techniques used back in the days. Since then, exceptions gained widespread acceptance as the standard way to deal with error conditions in many programming languages, including Java.
There is only one important rule related to exceptions handling (not only in Java): never ignore them! Every exception should be a least logged (please see Exceptions and logging) but not ignored, ever. Nonetheless, there are those rare circumstances when exception could be safely ignored because really not much could be done about it (please refer to the Using try-with-resources section for the example).
And one more thing, in the part 6 of the tutorial, How to write methods efficiently, we have discussed argument validation and sanity checks. Exceptions are a crucial part of these practices: every public method should verify all required preconditions before doing any real work and raise an appropriate exception if some of those have not been met.
3. Checked and unchecked exceptions
The exceptions management in the Java language differs from other programming languages. This is primarily because there are two classes of exceptions in Java: checked exceptions and unchecked exceptions. Interestingly, those two classes are somewhat artificial and are imposed by Java language rules and its compiler (but JVM makes no difference between them).
As a rule of thumb, unchecked exceptions are used to signal about erroneous conditions related to program logic and assumptions being made (invalid arguments, null pointers, unsupported operations, …). Any unchecked exception is a subclass of RuntimeException
and that is how Java compiler understands that a particular exception belongs to the class of unchecked ones.
Unchecked exceptions are not required to be caught by the caller or to be listed as a part of the method signature (using throws keyword). The NullPointerException
is the most known member of unchecked exceptions and here is its declaration from the Java standard library:
public class NullPointerException extends RuntimeException { public NullPointerException() { super(); } public NullPointerException(String s) { super(s); } }
Consequently, checked exceptions represent invalid conditions in the areas which are outside of the immediate control of the program (like memory, network, file system, …). Any checked exception is a subclass of Exception. In contrast to the unchecked exceptions, checked exceptions must be either caught by the caller or be listed as a part of the method signature (using throws
keyword). The IOException
is, probably, the most known one among checked exceptions:
public class IOException extends Exception { public IOException() { super(); } public IOException(String message) { super(message); } public IOException(String message, Throwable cause) { super(message, cause); } public IOException(Throwable cause) { super(cause); } }
The separation to checked and unchecked exceptions sounded like a good idea at the time, however over the years it turned out that it has introduced more boilerplate and not so pretty code patterns than solved the real problems. The typical (and unfortunately quite cumbersome) pattern which emerged within Java ecosystem is to hide (or wrap) the checked exception within unchecked one, for example:
try { // Some I/O operation here } catch( final IOException ex ) { throw new RuntimeException( "I/O operation failed", ex ); }
It is not the best option available, however with a careful design of own exception hierarchies it may reduce a lot the amount of boilerplate code developers need to write.
It is worth to mention that there is another class of exceptions in Java which extends the Error class (for example, OutOfMemoryError
or StackOverflowError
). These exceptions usually indicate the fatal execution failure which leads to immediate program termination as recovering from such error conditions is not possible.
4. Using try-with-resources
Any exception thrown causes some, so called, stack unwinding and changes in the program execution flow. The results of that are possible resource leaks related to unclosed native resources (like file handles and network sockets). The typical well-behaved I/O operation in Java (up until version 7) required to use a mandatory finally
block to perform the cleanup and usually was looking like that:
public void readFile( final File file ) { InputStream in = null; try { in = new FileInputStream( file ); // Some implementation here } catch( IOException ex ) { // Some implementation here } finally { if( in != null ) { try { in.close(); } catch( final IOException ex ) { /* do nothing */ } } } }
Nevertheless the finally
block looks really ugly (unfortunately, not too much could be done here as calling close method on the input stream could also result into IOException
exception), whatever happens the attempt to close input stream (and free up the operation system resources behind it) will be performed. In the section Exceptions and when to use them we emphasized on the fact that exceptions should be never ignored, however the ones thrown by close method are arguably the single exclusion from this rule.
Luckily, since Java 7 there is a new construct introduced into the language called try-with-resources which significantly simplified overall resource management. Here is the code snippet above rewritten using try-with-resources:
public void readFile( final File file ) { try( InputStream in = new FileInputStream( file ) ) { // Some implementation here } catch( final IOException ex ) { // Some implementation here } }
The only thing which the resource is required to have in order to be used in the try-with-resources blocks is implementation of the interface AutoCloseable
. Behind the scene Java compiler expands this construct to something more complex but for developers the code looks very readable and concise. Please use this very convenient technique where appropriate.
5. Exceptions and lambdas
In the part 3 of the tutorial, How to design Classes and Interfaces, we already talked about latest and greatest Java 8 features, in particular lambda functions. However we have not looked deeply into many practical use cases and exceptions are one of them.
With no surprise, unchecked exceptions work as expected, however Java’s lambda functions syntax does not allow to specify the checked exceptions (unless those are defined by @FunctionalInterface
itself) which may be thrown. The following code snippet will not compile with a compilation error “Unhandled exception type IOException” (which could be thrown at line 03
):
public void readFile() { run( () -> { Files.readAllBytes( new File( "some.txt" ).toPath() ); } ); } public void run( final Runnable runnable ) { runnable.run(); }
The only solution right now is to catch the IOException
exception inside lambda function body and re-throw the appropriate RuntimeException
exception (not forgetting to pass the original exception as a cause), for example:
public void readFile() { run( () -> { try { Files.readAllBytes( new File( "some.txt" ).toPath() ); } catch( final IOException ex ) { throw new RuntimeException( "Error reading file", ex ); } } ); }
Many functional interfaces are declared with the ability to throw any Exception from its implementation but if not (like Runnable), wrapping (or catching) the checked exceptions into unchecked ones is the only way to go.
6. Standard Java exceptions
The Java standard library provides a plenty on exception classes which are designated to cover most of the generic errors happening during program execution. The most widely used are presented in the table below, please consider those before defining your own.
7. Defining your own exceptions
The Java language makes it very easy to define own exception classes. Carefully designed exception hierarchies allow to implement detailed and fine-grained erroneous conditions management and reporting. As always, finding the right balance is very important: too many exception classes may complicate the development and blow the amount of the code involved in catching exception or propagating them down the stack.
It is strongly advised that all user-defined exceptions should be inherited from RuntimeException
class and fall into the class of unchecked exceptions (however, there are always exclusions from the rule). For example, let us defined exception to dial with authentication:
public class NotAuthenticatedException extends RuntimeException { private static final long serialVersionUID = 2079235381336055509L; public NotAuthenticatedException() { super(); } public NotAuthenticatedException( final String message ) { super( message ); } public NotAuthenticatedException( final String message, final Throwable cause ) { super( message, cause ); } }
The purpose of this exception is to signal about non-existing or invalid user credentials during sing-in process, for example:
public void signin( final String username, final String password ) { if( !exists( username, password ) ) { throw new NotAuthenticatedException( "User / Password combination is not recognized" ); } }
It is always a good idea to pass the informative message along with the exception as it helps a lot to troubleshoot production systems. Also, if the exception was re-thrown as the result of another exceptional condition, the initial exception should be preserved using the cause
constructor argument. It will help to figure out the real source of the problem.
8. Documenting exceptions
In the part 6 of the tutorial, How to write methods efficiently, we have covered the proper documentation of the methods in Java. In this section we are going to spend a bit more time discussing how to make exceptions to be a part of the documentation as well.
If the method as a part of its implementation may throw the checked exception, it must become a part of the method signature (using throws
declaration). Respectively, Java documentation tool has the @throws
tag for describing those exceptions. For example:
/** * Reads file from the file system. * @throws IOException if an I/O error occurs. */ public void readFile() throws IOException { // Some implementation here }
In contrast, as we know from section Checked and unchecked exceptions, the unchecked exception usually are not declared as part of method signature. However it is still a very good idea to document them so the caller of the method will be aware of possible exceptions which may be thrown (using the same @throws
tag). For example:
/** * Parses the string representation of some concept. * @param str String to parse * @throws IllegalArgumentException if the specified string cannot be parsed properly * @throws NullPointerException if the specified string is null */ public void parse( final String str ) { // Some implementation here }
Please always document the exceptions which your methods could throw. It will help other developers to implement proper exception handling and recovering (fallback) logic from the beginning, rescuing them from troubleshooting issues in production systems.
9. Exceptions and logging
Logging (http://en.wikipedia.org/wiki/Logfile) is an essential part of any more or less complex Java application, library or framework. It is a journal of the important events happening in the application and exceptions are the crucial part of this flow. Later in the tutorial we may cover a bit the logging subsystem provided by Java standard library however please remember that exceptions should be properly logged and analyzed later on in order to discover problems in the applications and troubleshoot critical issues.
10. What’s next
In this part of the tutorial we have covered exceptions, a very important feature of the Java language. We have seen that exceptions are the foundation of the errors management in Java. Exceptions make handling and signaling erroneous conditions quite an easy job and, in contrast to error codes, flags and statuses, once occurred, exceptions cannot be ignored. In the next part we are going to address a very hot and complicated topic: concurrency and multithreaded programming in Java.
11. Download the Source Code
This was a lesson on how and when to use Exceptions. You may download the source code here: advanced-java-part-8