Exception Handling Best Practices in Java

I am going to tell you what are the best practices for handling exceptions in Java applications. An exception is an event that occurs during the execution of a program that disrupts the normal flow of the program and the program terminates abnormally.

Types of Exception – the exception can be either a checked exception or an unchecked exception.

A method may not handle exception thrown within it and would instead throw it up the method call stack to let its caller know that an abnormal event occurred. It does so by declaring that exception in the throws clause of its declaration. For example, let’s consider the following Java classes:

class A {
    public Object doSomething(Object o) throws Exception{
        //...
    }
}
class B {
    public void doSomething() {
        try {
            Object param = ...
            A a = new A();
            Object o = a.doSomething(param);
            //...
        } catch(Exception e) {
            //do with the exception
        }
    }
}

So in the above, in class A, method doSomething() throws Exception, so class A throws Exception. The method doSomething() does not do anything with the exception, so when method doSomething() in class B calls method doSomething() of class A it has to either handle that exception using try{}catch{} block or it has to declare throws in the class declaration to throw exception to its caller.

The caller itself may handle the exception thrown by its callee or in turn propagate that to its own caller.

In the above example, the method doSomething() does not do anything with the exception, so when method doSomething() in class B calls method doSomething() of class A, it handles the exception in the catch{} block.

Or if again method doSomething() in class B does not want to handle exception but wants to throw exception to its caller then it has to do the following:

class B {
    public void doSomething() throws Exception {
        A a = new A();
        Object o = a.doSomething();
    }
}

A method may translate an exception thrown by its callee into another exception and throw the new exception (to its own caller).

So if method doSomething() in class B wants to throw AppException() from its method then it has to wrap that exception to AppException() for throwing it by following way:

public class AppException extends Exception {
  private static final long serialVersionUID = 1L;
  public AppException(Throwable t) {
    super(t);
  }
}

Now you can use the above AppException in the class B.

public class B {
  public void doSomething() throws AppException {
    try {
      Object param = ...
      A a = new A();
      Object o = a.doSomething(param);
      //...
    } catch (Exception e) {
      //translate the exception to AppException
      throw new AppException(e);
    }
  }
}

Best Practices in Exception Handling

1. Handle Exception Close to Its Origin

It does not mean “catch and swallow i.e. suppress or ignore exception”.
The following example is non-compliant – only exception message is logged

try {
    //...
} catch(AppException e) {
    Logger.info(e.getMessage());
}

The following example is non-compliant – only exception object is logged.

try {
    //...
} catch(AppException e) {
    Logger.info(e);
}

The following example is non-compliant – only context message is logged.

try {
    //...
} catch(AppException e) {
    Logger.info("context information");
}

It means “log and throw an exception relevant to the source layer”.

The following example is compliant – context message and exception object are present.

try {
    //...
} catch (Exception e) {
    throw new AppException("context information", e);
}

2. Throw DaoException from DAO layer and AppException from Business layer.

3. In applications using Web Services, the Web Service (a.k.a Resource) layer

  • Should catch all exceptions and handle them by creating proper error response and send it back to client.
  • Should not allow any exception (checked or unchecked) to be “thrown” to client.
  • Should handle the Business layer exception and all other unchecked exceptions separately.

Let’s look at the below example:

try {
    //...
} catch(AppException e) {
    // form error response using the exception’s error code and error message
} catch(Exception e) {
    // log the exception related message here, since this block is
    // expected to get only the unchecked exceptions
    // that had not been captured and logged elsewhere in the code.
    // form error response using the exception’s error code and/or error message
}

The catch handler for ‘Exception’ in the Web Service layer is expected to handle all unchecked exceptions thrown from within ‘try’ block

4. Log exception just once and log it close to its origin

Logging the same exception stack trace more than once can confuse the programmer examining the stack trace about the original source of exception.

try {
    //...
}    catch(AppException e) {
    // log the exception specific information
    // throw exception relevant to the source layer
}

5. When catching an exception and throwing it through an exception relevant to the source layer, make sure to use the construct that passes the original exception’s cause

For example,

try{
    //...
} catch(SQLException e) {
    // log technical SQL Error messages, but do not pass
    // it to the client. Use user-friendly message instead
    Logger.error("An error occurred when searching for the product details" + e.getMessage());
    throw new DaoException("An error occurred when searching for the product details", e);
}

6. In case of existing code that may not have logged the exception details at its origin. In such cases, it would be required to log the exception details but do not overwrite the original exception’s message with some other message when logging

Let’s look at the following example:

DAO Layer:

try{
    //...
} catch(SQLException e){
    //Logging missed here
    throw new DaoException("An error occurred when processing the query", e);
}

Since logging was missed in the exception handler of the DAO layer, it is mandatory in the exception handler of the next enclosing layer – in this example it is the (Business/Processor) layer.

Business/Processor Layer:

try{
    //...
} catch(DataAccessExceptione) {
    // logging is mandatory here as it was not logged
    // at its source (DAO layer)
    Logger.error(e.getMessage());
    throw new AppException(e.getMessage(), e);
}

7. Do not catch “Exception”

Accidentally swallowing RuntimeException, for example:

try{
    doSomething();
} catch(Exception e) {
    Logger.error(e.getMessage());
}

This code:

  • Also captures any RuntimeException that might have been thrown by doSomething() method
  • Ignores unchecked exceptions and
  • Prevents them from being propagated

So, all checked exceptions should be caught and handled using appropriate catch handlers. And the exceptions should be logged and thrown to the outermost layer (i.e. the method at the top of the calling stack) using application specific custom exceptions relevant to the source layer.

8. Handle Exception before sending response to the Client

The layer of code component(i.e. the method at the top of the calling stack) that sends back response to the client, has to do the following:

  • Catches all checked exceptions and handle them by creating proper error response and send it back to client
  • Does not allow any checked exception to be thrown to the client
  • Handles the Business layer exception and all other checked exceptions raised from within the code in that layer separately

Examples of such components are:

  • Service layer Classes in Web Service based applications
  • Action Classes in Struts framework based applications

Example:

try {
    //...
} catch(AppException e) {
    // create error response using the exception's error code and error message
}

Exceptional case 1 for handling Exception

There would be rare situations where users would prefer an easy to understand message to be displayed to them, instead of the system defined messages thrown by unrecoverable exceptions. In such cases, the method at the top of the calling stack, which is part of the code that sends response to the client is expected to handle all unchecked exceptions thrown from within the try block. By doing this, technical exception messages can be replaced with generic messages that the user can understand.

So, a catch handler for Exception can be placed in it. This is an exception to best practice 6 and is only for the outermost layer. In other layers downstream in the layered architecture, catching Exception is not recommended for reasons explained under best practice 6.

Example:

try {
    //...
} catch(AppException e) {
    // create error response using the exception's error code and error message
} catch(Exception e) {
    // Log the exception related message here, since this block is
    // expected to get only the unchecked exceptions
    // that had not been captured and logged elsewhere in the code,
    // provided the exception handling and logging are properly
    // handled at the other layers in the layered architecture.
    // create error response using the exception's error code and error message
}

Exceptional case 2 for handling Exception

Certain other exceptional cases justify when it is handy and required to catch generic Exception. These cases are very specific but important to large, failure-tolerant systems.

Consider a request processing system that reads requests from a queue of requests and processes them in order.

public void processAllRequests() {
    Request req= null;
    try {
        while(true) {
            req= getNextRequest();
            if(req!=null) {
                processRequest(req); // throws BadRequestException
            } else { // Request queue is empty, must be done
                break;
            }
        }
    } catch(BadRequestExceptione) {
        Logger.error("Invalid request:" + req, e);
    }
}

With the above code, if any exception occurs while the request is being processed (either a BadRequestException or any subclass of RuntimeException including NullPointerException), then that exception will be caught outside the processing while loop.

So, any error causes the processing loop to stop and any remaining requests will not be processed. That represents a poor way of handling an error during request processing.

A better way to handle request processing is to make two significant changes to the logic.

  1. Move the try/catch block inside the request-processing loop. That way, any errors are caught and handled inside the processing loop, and they do not cause the loop to break. Thus, the loop continues to process requests, even when a single request fails.
  2. Change the try/catch block to catch a generic Exception, so any exception is caught inside the loop and requests continue to process.
public void processAllRequests() {
    while(true) {
        Request req= null;
        try {
            req= getNextRequest();
            if(req!=null) {
                processRequest(req); // throws BadRequestException
            } else { // Request queue is empty, must be done
                break;
            }
        } catch(BadRequestExceptione) {
            Logger.error("Invalid request:" + req, e);
        }
    }
}

In situations where requests, transactions or events are being processed in a loop, that loop needs to continue to process even when exceptions are thrown during processing.

9. Handling common Runtime Exception

NullPointerException

  • It is the developer’s responsibility to ensure that no code can throw it
  • Run null reference checks wherever it has been missed

NumberFormatException, ParseException

  • Catch these and create new exceptions specific to the layer from which it is thrown (usually from business layer) using user-friendly and non technical messages
  • To avoid ClassCastException, check the type of the class to be cast using the instanceof operator before casting.
  • To avoid IndexOutOfBoundsException, check the length of the array before trying to work with an element of it.
  • To avoid ArithmeticException, make sure that the divisor is not zero before computing the division.

Example:

try{
    int item = Integer.parseInt(itemNumber);
} catch(NumberFormatException nfe) {
    Logger.error("Item number is invalid and not a number");
    throw new AppException("Item number is invalid and not a number", nfe);
}

All other unchecked exceptions i.e., RuntimeException, will be caught and handled by the Exception handler in the outermost or top layer.

Leave a Reply

Your email address will not be published.