Two Types of Exceptions

Java started out by borrowing the C++ exception mechanism. However, early in the development of the JDK, Sun made some important modifications. Instead of only one category of exception, Java has two. Today, Java differentiates between an Exception and a RuntimeException. To understand how this differentiation is advantageous, you must first understand these two types of exceptions.

The Exception Subclasses

When a method can throw an Exception, the exception must be caught in the body of the method or declared in the throws clause of the method declaration:

public void someDatabaseMethod ( ) throws SQLException {
 // Do some JDBC Work.
}


In this code, the method someDatabaseMethod( ) can throw a SQLException if there is a problem with the database. Since SQLException is a descendant of the Exception class, someDatabaseMethod( ) must either handle the exception with a try-catch block or declare that the method throws the SQLException. Since someDatabaseMethod( ) declares that it throws SQLException, any method calling someDatabaseMethod( ) is required to catch the exception or declare that it also throws the exception. This process of passing the responsibility to callers must be propagated down the call stack until the exception is actually handled. This mechanism allows code to clearly show the problems that may occur within a method and require the user of that method to handle them.

Superfluous exceptions

The problem with declaring a method in the throws clause is that these exceptions can become lengthy and superfluous. If you have ever encountered a method that threw five or six exceptions, you know exactly what I mean. Since you have to catch exceptions or rethrow them, using third-party libraries that throw a lot of exceptions can become a pain. In fact, there are instances when throwing even one exception is an annoyance. Consider the basic JDBC program in Example 5-1 to get a good idea of what I mean.

Example 5-1. Throwing superfluous exceptions
package oracle.hcj.exceptions;
public class SuperfluousExceptions {
 public void prepareJDBCDriver( ) throws ClassNotFoundException {
 Class.forName("com.mysql.jdbc.Driver");
 }
}


In a JDBC program, the first thing you need to do is load the database driver into the virtual machine by using the Class.forName( ) call with the name of the database driver class as the parameter. This will call the static initializers of the driver class. However, it is possible that the named class does not exist in the classpath. If the virtual machine cannot find the class, the call Class.forName( ) throws a ClassNotFoundException. This type of error becomes a problem only if it happens routinely. In fact, it should occur only if the user has a classpath problem; proper usage of the method will never result in an exception being thrown. Yet this exception will still often need to be caught or declared that it is thrown by your method. Luckily, in Example 5-1, you have only one superfluous exception that you have to catch. However, once you use reflection (see ), you will find that there are several exceptions that can complicate your code. Example 5-2 shows a problematic method using reflection.

Example 5-2. Catching multiple superfluous exceptions
package oracle.hcj.finalstory;
public class SuperfluousExceptions {
 public void reflectionMethod( )
 throws NoSuchMethodException, IllegalAccessException,
 InvocationTargetException, ClassNotFoundException, 
 MyCustomException {
 Class.forName("oracle.hcj.bankdata.Gender");
 // . . . some code
 Method meth = Gender.class.getDeclaredMethod("getName", null);
 meth.invoke(Gender.MALE, null);
 // . . . other code working with result that may // throw a program exception
 }
}


This code shows how things can become problematic with multiple exception types in a throws clause. In Example 5-2, the only exception that is particularly important is MyCustomException, which indicates a violation of a business rule. If any other exceptions are thrown, it would indicate a configuration or coding error that shouldn't happen routinely. However, since all of the other exceptions listed could occur and are direct descendants of Exception, you must either declare them or handle them. This makes the code bulky, hard to read, and generally annoying. Furthermore, declaring your method to throw Exception or Throwable (the parent exception) isn't a good solution, either. The problem with declaring that a method throws Exception or Throwable is that you cloud the dynamics of your method by complicating the problems that can occur in that method. For example, if reflectionMethod( ) was declared with throws Exception, and you called it from another method, you could end up with something like the following:

 public void someMethod( ) throws Exception {
 try {
 // .. some code that starts a purchase transaction..
 reflectionMethod( );
 } catch (final Exception ex) {
 if (ex instanceof MyCustomException) {
 if (((MyCustomException)ex).getType( ) == MyCustomException.CREDIT_CARD_DECLINED) {
 // check if customer has other cards,
 // if not throw a billing error. throw new MyCustomException(MyCustomException.BILLING_ERROR);
 }
 } else {
 throw ex;
 }
 }
 }


In this code, the only exception that someMethod( ) is interested in catching is the MyCustomException with a CREDIT_CARD declined type. If it catches this exception, someMethod( ) will then try to use other credit cards that the customer has; if it doesn't, then it will transform the exception type into a BILLING_ERROR. The main problem with this method is that since any kind of Exception can be thrown in the try block, the method must rethrow these exceptions or deal with them. Therefore, someMethod( ) also has to declare a throws clause for Exception. This problem quickly propagates in your code until practically every method is declared with throws Exception. Furthermore, this code masks any exceptions that may be legitimate within someMethod( ), such as a NullPointerException. To avoid this situation, you need to filter your exceptions so that you don't have to declare them in the throws clause, but still maintain differentiated exceptions that can propagate down the call stack. This is why there is a second type of exception, the RuntimeException subclasses.

The RuntimeException Subclasses

RuntimeException and its subclasses are treated differently than Exception and its direct subclasses by the compiler and virtual machine. Unlike Exceptions, RuntimeExceptions are not required to be declared in the throws clause of the method. These types of exceptions indicate a problem that should not routinely happen. NullPointerException and IllegalArgumentException are good examples of RuntimeException subclasses that indicate that something unexpected happened in the code, and that the problem is unrelated to business logic or user error. Both of these exception types indicate a coding error and not something that would normally happen in the course of the program. Interestingly, RuntimeException inherits directly from the Exception class, and it is ignored in a throws clause, a feature that is built into the Java virtual machine and compiler. RuntimeExceptions allow you to clean up the method in Example 5-2 and turn it into something more manageable:

package oracle.hcj.finalstory;
public class SuperfluousExceptions {
 public void reflectionMethod2( ) throws MyCustomException {
 try {
 Class.forName("oracle.hcj.bankdata.Gender");
 // . . . some code
 Method meth = Gender.class.getDeclaredMethod("getName", null);
 meth.invoke(Gender.MALE, null);
 // . . . other code working with result that may // throw a program exception
 } catch (final NoSuchMethodException ex) {
 throw new RuntimeException(ex);
 } catch (final IllegalAccessException ex) {
 throw new RuntimeException(ex);
 } catch (final InvocationTargetException ex) {
 throw new RuntimeException(ex);
 } catch (final ClassNotFoundException ex) {
 throw new RuntimeException(ex);
 }
 }
}


In reflectionMethod2( ), you use RuntimeException with the chained exception facility from . This allows you to filter the exceptions that would indicate configuration or programming problems and pass them on without declaring them in the throws clause; you can declare only MyCustomException in the throws clause without causing any problems. Implement the filter by catching each exception that would indicate a coding error and rethrow it inside of a RuntimeException.

Masking exceptions

Although filtering exceptions is a powerful technique, there is a danger to filtering exceptions that trips up many unaware developers:

package oracle.hcj.finalstory;
public class SuperfluousExceptions {
 public void reflectionMethod3( ) throws MyCustomException {
 try {
 Class.forName("oracle.hcj.bankdata.Gender");
 // . . . some code
 Method meth = Gender.class.getDeclaredMethod("getName", null);
 meth.invoke(Gender.MALE, null);
 // . . . other code working with result that may // throw a program exception
 } catch (final Exception ex) {
 if (ex instanceof MyCustomException) {
 throw (MyCustomException)ex;
 }
 throw new RuntimeException(ex);
 } }
}


Although this code looks fine from a superficial point of view, it is horrendous from a maintenance point of view. The first problem is that this method effectively masks the throwing of all exception types except for your custom exception. This may seem like a good idea; however, you may have code in the method that throws a java.text.ParseException. This exception may indicate that the user typed an unreadable date format in a field. This is a business logic exception that should be declared on the method line so that callers will be aware of the possible error and account for it. However, since you globally mask all exception types, you unintentionally turn this business logic exception into a RuntimeException. As a result, the compiler doesn't warn you that you haven't declared the exception, developers don't account for it, and you end up having to perform repairs throughout your entire program six weeks later.

Ignoring exceptions

If masking exceptions bothers you, you should find this next example downright disturbing:

package oracle.hcj.finalstory;
public class SuperfluousExceptions {
 public void reflectionMethod3( ) throws MyCustomException {
 try {
 Class.forName("oracle.hcj.bankdata.Gender");
 // . . . some code
 Method meth = Gender.class.getDeclaredMethod("getName", null);
 meth.invoke(Gender.MALE, null);
 // . . . other code working with result that may // throw a program exception
 } catch (final Exception ex) {
 if (ex instanceof MyCustomException) {
 throw (MyCustomException)ex;
 }
 } 
 }
}


If group masking of exceptions is bad, then ignoring exceptions is absolutely evil. In this code, the exceptions aren't even rethrown as RuntimExceptions; they are dumped and disappear altogether. There are very few examples of situations in which it is okay to completely ignore exceptions. Even these few examples don't allow the user to ignore exceptions in a large block of code. When you ignore exceptions, you are pretending that the exception isn't an error. This should be done only if you are certain that it isn't an error. One of these rare examples is shown in Example 5-3.

Example 5-3. An example of exceptions that can be legitimately ignored
package oracle.hcj.finalstory;
public class SuperfluousExceptions {
 public void reflectionMethod5(final String[] packageList)
 throws MyCustomException {
 try {
 Class.forName("oracle.hcj.bankdata.Gender");
 // . . . Reflection based code
 // . . . other code working with result that may // throw a program exception
 // try to load a class by searching through some packages.
 for (int idx = 0; idx < packageList.length; idx++) {
 try {
 Class.forName(packageList[idx] + "MyClass");
 } catch (final ClassNotFoundException ex) {
 // not an error. 
 }
 }
 } catch (final NoSuchMethodException ex) {
 throw new RuntimeException(ex);
 } catch (final IllegalAccessException ex) {
 throw new RuntimeException(ex);
 } catch (final InvocationTargetException ex) {
 throw new RuntimeException(ex);
 } catch (final ClassNotFoundException ex) {
 throw new RuntimeException(ex);
 }
 }
}


In reflectionMethod5( ), the ClassNotFoundException is ignored during the for loop. The goal of the emphasized block of code is to look through a search path of packages and load a class that may be in one of these packages. In this case, a ClassNotFoundException in an iteration of the loop is definitely not an error, but a legitimate potential outcome. This is one example of a situation in which it is legitimate to ignore exceptions. However, note that even in this case, the try-catch block contains only the single line of code that attempts to load the class. You should also note that only the specific exception that is known to be an error is caught and ignored. Finally, later in the method, notice that any other ClassNotFoundException exceptions that may be thrown are filtered. Unlike the ClassNotFoundException instances that could be thrown in the for loop, instances thrown outside of the for loop indicate an error and should be propagated. While filtering exceptions, you should always watch for unintentional side effects, such as masking and ignoring exceptions. For this reason, it's best to be as explicit as possible in your code. Using multiple catches to catch specific exceptions will make your code a bit longer, but the advantages far outweigh the costs.

Exception or RuntimeException

Now that you have the concepts of Exception and RuntimeException types clearly in mind, the only decision left is to figure out when to make a class a subclass of Exception and when to make it a subclass of RuntimeException. Generally, this depends on the circumstances and the nature of the exception. If you consider NullPointerException, you know that this exception always indicates a coding error. However, with IllegalAccessException, the case is less clear. In some circumstances, such as during authorization control, it is a legitimate logic error. In other circumstances, such as in reflectionMethod( ), it indicates a programmer error. Although whether to make an exception a RuntimeException or an Exception is something of a judgment call, the following guidelines should help:

  • If an exception is always the result of a coding error, make it a direct descendant of RuntimeException.
  • If an exception represents a user's failure to enter data properly, you may be able to get away with using RuntimeException. This will allow you to propagate the RuntimeException in business logic classes without having to declare it. However, the GUI or other interface to the program should always watch for these kinds of errors. We will discuss one of these types of exceptions in .
  • If the exception is always the result of a business logic error, make it a direct descendant of Exception and use exception filtering when appropriate.
      
Comments