Making an Exception-Handling Mechanism
Follow one dev's thoughts on how you should handle exceptions, ranging from dealing with your catch blocks to using try-with-resources to avoiding using them at all.
Join the DZone community and get the full member experience.
Join For Free“When used to best advantage, exceptions can improve a program’s readability, reliability, and maintainability. When used improperly, they can have the opposite effect.”
– Effective Java
Every good developer aims to write good code. So good that the system running it will not crash ever, and everything will work out smooth and quiet, and everyone will be happy. To make this close to reality, a lot of factors need to be taken into account, because Murphy's Law always raises its ugly head. For instance, “Whenever you need a crucial file from the disk, it will not be there.” So, what’s next? The system will crash and that’s it, you’re done! What I know for sure is that you can’t prevent this situation itself, but you must’ve expected this and implemented some way of handling it. There are thousands and thousands of situations like this, when you must think about what can go wrong. Is it a physical resource that could be missing? A user error? Who knows, but you must somehow treat these exceptional conditions.
Normally, when something goes wrong, under exceptional conditions, an exception is created. Fortunately, Java provides us with a strong exception-handling mechanism. Let’s quickly examine the following diagram, which shows us exception types and their hierarchy
As you may notice, an exception is nothing more than a Java class derived from the Throwable class. Straight to the point:
Error: These are abnormal conditions that are beyond the programmer’s control — because there is nothing that you can do about an error that arises. So the important thing to remember is that you should never try to treat an error in any way.
Exception: Parent class for all existing exceptions in the world, which, in turn, has two subclasses:
- Checked Exceptions: These are typically used when you want to force the programmer using your API to think of how to handle them. These types of exceptions are checked at compile-time. Any subclass of the Exception class, which is not a subclass of RuntimeException, is considered a checked exception.
- Runtime Exceptions: Also often called Unchecked Exceptions, these are used when your exceptional condition is unrecoverable, and you can’t do anything about this. The programmer is not forced to handle these types of exceptions.
Now let’s dig a bit deeper into the technical details and examine Java exception handlers.
Exception Handling
We will focus now on Java exception handlers and their possibilities. For runtime exceptions, it is not mandatory to write exception handlers, though the programmer can do that. Basically, there are several ways to deal with exceptions.
- Wrapping a piece of code into a try block and specify the exception handler in a catch block. Optionally, you can specify a finally block. Usually, a finally block is used to close all the resources that are no longer necessary.
try{
//some tough code
} catch(SomeException e){
//handle exception
} finally {
//close resources
}
The execution flow is simple: try is executed (also called the guarded block), and if an exception is thrown from the try block, and there is an exception type match in any of the catch clauses, the catch block will be executed right away. The finally block will always be executed, whether or not an exception was raised.public void doSomething() throws SomeException {
//code that can throw SomeException
}
When to Use Either
Usually, when writing some code that might throw an exception, ask yourself, is this your responsibility, and can you do something with this. Can you handle this somehow? If you answer even slightly yes, then you have to wrap it into a catch block and handle it. Otherwise, just pass it down to the call stack. In fact, nobody forces you to write the complete handler. You can take your part of the responsibility and rethrow the exception, like in the following example:
private void createUser(User user) throws ValidationException {
try{
userService.create(user);
logger.info(“Create new user {}” , user.getUserName());
} catch (ValidationException e) {
logger.error(“Invalid data for creating new user {}, cause: {} ”, user.getUserName(), e.getMessage());
throw e;
}
}
This is a common pattern named “handle and declare.” We want to do something with the exception and handled it, but because we couldn’t handle it completely, we rethrow it and pass it to the caller.
Multicatch Block
Since Java 7 was released, we've been encouraged to use multi-catch statements in order to avoid code duplication. But this statement has a limitation. You cannot catch two exceptions if there is an inheritance relationship between them.
try{
//some code
} catch (NumberFormatException | IllegalArgumentException e){ // won't compile
}
Try-With-Resources
Another important feature that was introduced in Java 7 is called automatic resource management. Let’s examine the following code for reading from a file:
BufferedReader br = null;
try{
br = new BufferedReader(new FileReader(path));
//read from file
} catch (IOException e){
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
It looks a bit ugly, especially the finally block, but it's perfectly legal — and also necessary because you need to close the resources you are no longer using. In our case, we have to close the buffered reader. Using try-with-resources will transform the above code into something like this:
try (BufferedReader br = new BufferedReader(new FileReader(path))){
//read from file
} catch (IOException e) {
e.printStackTrace();
}
I’d say, this looks a lot better. To use this syntax, any class that participates in the try-with-resources statement should implement interface AutoCloseable, which looks like this:
public interface AutoCloseable {
void close() throws Exception;
}
It is recommended that all classes that implement this interface declare more specific exception types in their close() method signature than their parent class does. Otherwise, don't declare anything if invoking their close() method will not throw any exception.
Inappropriate Use of Exceptions
First of all, remember, there is a reason why they are called exceptions — because they happen only under exceptional conditions. Often, programmers use exceptions just to alter normal execution flow, which is incorrect. If you can add a check or just return a boolean value instead of using exceptions, then do so! In any case, do not use them just because you want to, like in the following example:
//some complex code
User user = userDao.getById(userId);
try {
user.getUserName();
} catch (NullPointerException e) {
logger.info(“No user found with ID {} ”, userId);
}
That’s really, really bad code. You shouldn’t catch a NullPointerException, which is a subclass of RuntimeException. Instead of this you, could add a simple check to the null object reference, something like this:
User user = userDao.getById(userId);
If (user == null) {
logger.info(“No user found with ID {} ”, userId”);
}
Plus, exception handlers slow down the execution of the program. When an exception is thrown, the following happens:
- The normal execution flow of the program is suspended.
- JVM searches for the appropriate exception handler in this class by looking at catch clauses from the top down.
- If no matching catch clause is found for the exception type, then it will search for the supertype of the exception.
- If no appropriate handler is found, the exception is passed down the call stack,
As you can understand, this will end in one of two ways. Either someone from the callers on the call stack will have the right exception handler, or the propagation of the uncaught exception will get to the main entry point of each Java application, in the main function, where it will terminate the execution of your program. Uncaught exceptions in a specific thread, when using multithreading, will be described later in this article.
Ignoring Exceptions
Often, instead of dealing with exceptions and handling them, many programmers tend to ignore them.
try{
//code
} catch( SomeSpecificException e) {
//ignoring
}
This is a bad practice because something exceptional happened, but you didn’t handle it, and this can end up with really serious bugs, system failures, etc. There are a few cases when there really is nothing to do with the caught exception, but at least you can add a log line explaining why you ignored the exception and log the stack trace.
All-in-one Catch
Another common mistake is catching the parent type of all exceptions in the (Java) world: the Exception class.
try{
//really complex code here
} catch(Exception e) {
}
This is not a correct approach. Imagine that you have complex code written in a try block, where different types of exceptions could be raised. The code above will catch every single exception that will ever be thrown from the try block and handle them in the same way. It doesn’t matter if it’s a checked exception or a runtime exception. It is always advised to catch specific exceptions that can be thrown from a corresponding try body. This offers you the possibility to handle each exception type in a different way.
Tricky Examples
1. Finally is not executed if the System.exit method is called inside a try block. This is good to know in order to avoid locking some external resources.
public class ExceptionHandling1{
public static void main(String[] args){
try{
System.out.println("Hello from try block");
System.exit(0);
} finally {
System.out.println("Hello from finally block");
}
}
}
Output
Hello from try block
2. Finally is executed even if a return statement is used inside a try block. In this case, we can rely on such implementation and be sure that the used resources are closed.
public class ExceptionHandling2{
public static void main(String[] args){
try{
System.out.println("Hello from try block");
return;
} finally {
System.out.println("Hello from finally block");
}
}
}
Output
Hello from try block
Hello from finally block
3. Any exception thrown in a static init block is wrapped into ExceptionInInitializerError. An ExceptionInInitializerError is thrown to indicate that an exception occurred during evaluation of a static initializer or the initializer for a static variable.”
public class ExceptionHandling3{
static{
throwRuntimeException();
}
private static void throwRuntimeException() {
throw new NullPointerException();
}
public static void main(String[] args) {
System.out.println("Hello World");
}
}
Output
java.lang.ExceptionInInitializerError Caused by: java.lang.NullPointerException at exception.test.ExceptionHandling3.throwRuntimeException(ExceptionHandling3.java:13) at exception.test.ExceptionHandling3. (ExceptionHandling3.java:8)
4. When using multithreading, one of the threads might throw an exception that will not be handled anywhere. Let’s assume we have the following code:
public class ExceptionHandling4
{
public static void main(String[] args) throws InterruptedException
{
Thread t = new Thread() {
@Override
public void run()
{
throw new RuntimeException("Testing unhandled exception processing.");
}
};
t.start();
}
}
Output
Exception in thread “Thread-0” java.lang.RuntimeException: Testing unhandled exception processing. at exception.test. ExceptionHandling4$1.run(ExceptionHandling4.java:27)
For this situation, for each thread, you can set an uncaughtExceptionHandler, which will handle the exception if it gets propagated out of the thread’s scope:
t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
System.out.println("Handled uncaught exception in thread :" + t + " Exception : " + e);
}
});
Output
Handled uncaught exception in thread :Thread[Thread-0,5,main] Exception : java.lang.RuntimeException: Testing unhandled exception processing.
This is important to know because throwing an exception from a thread can terminate your entire application (if this was the only non-daemon thread that just terminated).
Remember, as Murphy’s law states: “If something can go wrong, it will.” Do not hesitate to handle exceptions, (if that's the case, of course), do not ignore them and don’t use them where you can avoid them. Their purpose is to improve the reliability of your system by handling and treating exceptional conditions that can appear during the execution of your program.
Opinions expressed by DZone contributors are their own.
Comments