Wednesday, February 29, 2012

Java Exceptions Best Practices

Exceptions were introduced into the Java language to separate the functional code from error-handling code. They allow for clear propagation path of a specific error.
There are two types of exceptions: checked exceptions - compiler enforced exceptions that are instances of the Exception class or one of its subclasses and the unchecked exceptions, runtime exceptions like RuntimeException and its subclasses and Error and its subclasses.
A compiler for the Java programming language checks, at compile time, that a program contains handlers for checked exceptions.

Many times in my carrier as a software developer I had to read, debug, review code from some other developers. Many times I've seen silenced exceptions like:


try {
    someFunction();   // may throw an exception 
} catch (Exception e) {                  
    // do nothing}

I believe this type of code is Evil. Something happens in the code and the developer decides that the best way to go is to do nothing. If this code influences other code there will be no way to know what really happened with the code. If the code does not influence other code it is still no way to know that some functionality was not executed. The least a programmer can do in such situation is to write some minimal information to the log file.
Imagine that you are not able to debug a code that runs in production environment, the only means to investigate a problem is the log files. Every time a checked exception is correctly handled your job will be easier to investigate potential issues.

I have next a number of advices to follow when dealing with exceptions:
  • NEVER SILENCE AN EXCEPTION like in my above example.
  • Only throw checked exceptions (not derived from RuntimeException), if the caller has a chance to handle it.

     class ApplicatioException extends Exception { // classic checked exception
        public ApplicationException (String str) {
            super (str);
        }
     }

     ...
     
     class  Application {
        public void doSomeAction () throws ApplicationException {
            ...
            if (bad) {
                throw new ApplicationException ();
            }
        } 

        public void someOtherAction () {
            try {
              this.doSomeAction();                            
            } catch (ApplicatioException ex) {
              logger.error("doSomeAction failed miserably!"); // log information
            }
        }
     }
     
  • Checked exceptions are an official part of the interface, therefore do not propagate checked exceptions from one abstraction layer to another, because usually this would break the lower abstraction. E.g. do not propagate SQLException to another layer, because SQLExceptions are an implementation detail, that may change in the future and such changes should not affect the interfaces and their callers.
     class DBUtil{
        public static void closeConnection
        (Connection conn){
        try{
            conn.close();
        } catch(SQLException ex){
            throw new DBUtilException(ex); // propagate exception to the next level
        }
     }
  •  Never throw NullPointerException or RuntimeException. Use either IllegalArgumentException, or NullArgumentEception (which is a subclass of IllegalArgumentException anyway). If there isn't a suitable subclass available for representing an exception, create your own.
     class DBUtil{
        public static void closeConnection
        (Connection conn){
           try{
              conn.close();
           } catch(SQLException ex){
               throw new RuntimeException(ex); // never throw an exception like this    
        }
     }
        
  • Only if it is not possible to return special result values cleanly, use checked exceptions to force the caller to decide the situation. The caller should deescalate the situation by catching and handling one or more checked exceptions, e.g. with special result values or by escalating with an unchecked exception, because the situation is an error, that can not be handled.
  • Exceptions that signal programming errors or system failures usually cannot be handled/repaired at runtime -> unchecked exception.
  • Do NOT throw an exception, if you only suppose the caller of your code could have a problem with a special result. Try to return a special result value instead e.g., null, and let the caller decide with a regular if-else-statement. If the caller really has a problem, HE WILL throw an exception on his own.
    class Example{
        public Result exampleAction (){
            Result result = null;
            ...                               // some result processing
            return result;                    // return the result in any form
            }
        }
        
        public boolean processResult () throw ResultException {
            Result result = exampleAction();

            if (result == null) {
                return new ResultException (); // caller has a problem here; unexpected result
            }
            else if (!isValid(result)) {
                return fail;                   // result failure
            }
             
            return success;
        }
     }

  • The intention of exception-handling is to separate real error-handling from the regular part of the code, so don't force the caller to mix it with unnecessary exceptions.
  • Only if your code really has a problem to continue e.g., when a parameter is invalid, feel free to throw an exception!
  •  Don't catch generic exceptions. Sometimes it is tempting to be lazy when catching exceptions and do something like this:
    try {
        someIOFunction();        // throws IOException 
        someParsingFunction();   // throws ParsingException 
        someSecurityFunction();  // throws SecurityException  
    } catch (Exception e) {      // catch all exceptions 
        handleError();           // with one generic handler!
    }
    
    
    You should not do this. In almost all cases it is inappropriate to catch generic Exception or Throwable. Throwable includes Error exceptions as well. It is very dangerous. It means that Exceptions you never expected (including RuntimeExceptions like ClassCastException) end up getting caught in application-level error handling. It obscures the failure handling properties of your code. It means if someone adds a new type of Exception in the code you're calling, the compiler won't help you realize you need to handle that error differently. And in most cases you shouldn't be handling different types of exception the same way, anyway.
I believe proper exception handling is a good indicator that a programmer understands the programming language he/she uses and is able to do good job.