Saturday, March 30, 2013

Best Practices in Java Exception Throwing, Handling, and Logging


Exception Related Terminology

In an object-oriented program language such as Java, C#, and JavaScript, an exception is an object that represents an abnormal event. When one method (caller) calls another method (callee), they may communicate about what happened via an exception:
  1. A method may throw an exception to let its caller know that an abnormal event occurred. We call it exception throwing. 
  2. A method may propagate an exception thrown by its callee to its own caller by declaring that exception in its throws clause of method declaration.
  3. A method may translate an exception thrown by its callee into another exception, and throw the new exception (to its own caller)
  4. A method may catch an exception thrown by, or propagated from, its callee, or an exception thrown by it. When a method catches an exception, it may do something in response to the exception. Doing something in response to an exception is called exception handling.
If an exception goes through the main method of a Java or C# program, the program crashes.

Why Throw and Handle Exception

Essentially, a method throws or propagates an exception to its caller to let it know something is wrong, and let the caller know it is time for it to do something to help (i.e. to handle the exception).
Obviously, in almost all cases, it is beneficial to handle exceptions to avoid:
  1. Program crashing
  2. Data corruption
  3. Resource hugging – hugging threads, unclosed database connections, unclosed socket connections, leaked memory, etc.
  4. No response or meaningless response to end users
  5. Any other ways to leave all or part of a system in invalid states.

Exception Throwing

Don’t Throw Exception When Nothing is Abnormal

Exception is supposed to be used to alert one’s caller that something is wrong and the caller should go through an exceptional flow path to handle it. Throwing an exception when nothing is abnormal just like raising fire alarm when you only try to tell your colleague that you need his/her help to make a copy. It works, but is wrong, confusing normal conditions with exceptional conditions. An exception handling flow path is not just another path. It costs much more in CPU time and other resources. It also confuses reader of the code, making it harder to see what the code really intents to do.

For more discussion, please see Item 57: Use exceptions only for exceptional conditions in Chapter 9. Exceptions in Joshua Bloch’s Effective Java, Second Edition.

Do Throw Exception When Something is Abnormal

In a programming language that does not have the exception support (i.e. the try/catch/finally construct), people tend to overload procedure (function, subroutine, method, etc) output (function return or output parameter) with error code. In such practice, certain values of the output are going to be interpreted as normal output and others as error code. There are many disadvantages in such practice:
  • It is more difficult to understand the procedures with output overloading of error code
  • The caller (client) must explicitly check the output to determine its appropriate response
  • Client code that handles the normal case and exception/error case tends to mingle together
The exception support in modern programming languages is just to overcome above problems. When we programming in a modern programming language, we should take advantage of its exception support and throw exception when something is abnormal.

Avoid Triggering Exception

When one (method) calls another method, the caller should be coded in such a way to avoid triggering exceptions thrown by the callee. If the callee does not throw exceptions, the caller don’t have to handle them, to propagate them, or to translate them. Following are ways to avoid triggering exceptions:
  • To avoid NullPointException,  make sure that an object is not null before invoke a method on it.
  • To avoid ClassCastException,  check type of the class to be casted 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.
In addition, frequently, a callee throws exception because arguments passed to it are not valid. To remedy such trouble, there should be good document about the callee in regard to what are valid arguments to it, and the caller should ensure the arguments are valid before pass them to the callee.
Similarly, end user input should be validate as early as possible, and the program should raise flag with sufficient information to the user as early as possible given certain user input are invalid rather than allowing the invalid input propagate down the stream further. The farther where a symptom shows up away from the root cause (here invalid user input), the harder to identify the real cause.

Checked or Unckecked Exception 

When one method throws an exception, that exception can be either a check exception or an unchecked exception. An exception that directly or indirectly extends java.lang.RuntimeException is an unchecked exception. An exception that directly or indirectly extends java.lang.Exception but not java.lang.RuntimeException is a checked exception. When one method throws a checked exception, it is mandatory for its caller to either catch the exception or propagates it to its own caller. All checked exception that a method may throw must appear in the throws clause of the method’s API. Initially it was thought best practice to only use checked exception because the benefits of easily seeing what exceptions can be thrown by a method and forcing the method’s callers to handle (or propagate) the exception. After many years of practices, a lot of Java programmers, include Joshua Bloch, Rod Johnson, and Bruce Eckel, came to realize that frequently a caller cannot really do much in handling exceptions thrown by its callee but is forced to have a certain amount of code to get around the mandatory. It costs developers’ time to add that code that doesn’t really do anything useful but clusters the code and confuses readers.

In short, my recommendation on determining between throwing checked or unchecked exception is as following:
  • A method should throw a checked exception when it can reasonably expect the caller to do something to recover from the exception.
  • A method should throw an unchecked exception when it can reasonably assumes the caller cannot do anything to recover from the exception
For more discussion on this issue, please see Item 58: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors and Item 59: Avoid unnecessary use of checked exceptions in Chapter 9. Exceptions in Joshua Bloch’s Effective Java, Second Edition.

The IBM DeveloperWorks article Java theory and practice: The exceptions debate also provides a summary of the checked v.s. Unckecked exception debates.

Exceptions Should Be In Terms of Caller’s Perspective

The purpose for a method to throw an exception is to let the caller know that something abnormal occurred. That something must be in the caller’s perspective, in other words, at the same abstraction level as other things in the implementation of the caller. For example, I am using a computer. In this case, I am the caller and the computer the callee. I “call” the computer to do many useful things for me. If suddenly the computer, instead of doing something I expected, shows me some message like F^&*(%$HNBVFER2U74^$4, I won’t have any clue about what is going on and what I should do in response. If, instead, the error message is “The second RAM module is corrupted”, then I know much better what is going on and what I should do in response. The first error message may mean something tor an electronics engineer specialized in RAM but is not at the level of abstractions known by an ordinary computer user.

For more discussion on this issue, please see Item 61: Throw exceptions appropriate to the abstraction in Chapter 9. Exceptions in Joshua Bloch’s Effective Java, Second Edition.

Exception Message

When one creates a new exception object, one passes a string message into the constructor. The message can be retrieve later to gain helpful information. It helps to have adequate information in the message to be written into log file and to help developers and administrators to reproduce and diagnose the exception later. Please note that such message is normally not for the caller to use because it is normally in natural language and it is hard for the caller (a program module) to understand.

For more discussion on this issue, please see Item 63: Include failure-capture information in detail messages in Chapter 9. Exceptions in Joshua Bloch’s Effective Java, Second Edition.

Document Exceptions Thrown in Javadoc

For each method that throws checked exceptions, in its Javadoc, document each exception thrown with a @throws tag, including the condition under which the exception is thrown.

For more discussion on this issue, please see Item 62: Document all exceptions thrown by each method in Chapter 9. Exceptions in Joshua Bloch’s Effective Java, Second Edition.

Exception Handling

When an exception is thrown by a method itself or its callee, the method may have to do one or more of the following to handle the exception:
  1. Do something to recover completely from the exception so its own caller does not have to notice the occurrence of the exception. It is ideal but rare.
  2. If the method cannot completely recover the system from the exception, do as much as possible to bring the system into a consistent state or roll it completely back to the original state, including release resources held for those operations that triggered the exception.
  3. If it is beneficial for its own caller to know the occurrence, it can re-throw the same exception, or wrap it in another exception and throws it as long as it is more appropriate. It is appropriate when the new exception can help its own caller to know better what happened from the caller’s perspective. Use Java 1.4 exception chaining mechanism to preserver root exception information.
  4. When it may help developers and system administrators on further investigating an exception and fixing the problem offline, log the exception, with stack trace, exception message,  and as much as possible details of the context (largely the state of “this” object and parameters to “this” method) where the exception is thrown by its callee or it. Most time, it is beneficial to do both logging and translation/wrapping (or re-throwing).

At lease a method at the top of the calling stack in that thread should log the exception to one or more log files. Usually only the method at the top of a calling stack in a particular thread should be allowed to halt the thread.

Recovering from Exceptions

In some cases, a method may recover from an exception. For example, inside a method, a trial has been made to open a network socket and an exception has occurred indicating that no socket could be opened. More trials can be made. If the later trial succeeded, the method recovered from its exception, and its own caller does not need to know the occurrence of the exception.

Mitigating Exception Impacts

In some cases, a method cannot completely recover from an exception. In such cases, the method should do as much as possible to:
  1. Bring the system into consistent state or the original state, so the system can keep running without corrupting data and late operations
  2. Release resources hold for the operations that triggered the exception. For example, in case of database exception, close the database connection to prevent database connection leaks. Other possible resources leaks are file handler leaks, socket leaks, memory leaks, etc. See more in the Release Resource in finally Block sub-section below. In such cases, most likely, the method should also need to let its own caller know the occurrence of the exception in order for its own caller to do its own share. If the original exception is equally at the same abstraction level of its own caller, re-throw the same exception. Otherwise, wrap the original exception into another exception that is at the abstraction level of its own caller and throw the other exception. Use Java 1.4 exception chaining mechanism to preserver root exception information.
For more discussion on this issue, please see Item 64: Strive for failure atomicity in Chapter 9. Exceptions in Joshua Bloch’s Effective Java, Second Edition.

Release Resource in finally Block

In case that a method needs to release resources hold no mater exceptions are thrown or no, release the resources in the finally block of try/catch/finally construct. The finally block will be executed no matter the method finishes normally or with exception.

No Blank Catch Clause

In general, it is a very bad practice to have blank catch clause for exceptions. A blank catch clause does not really recover or mitigate an exception but prevents other from doing so. Only for rare cases, such practice is acceptable. Acceptable cases are:
  • exception during close JDBC ResultSet, Statement, and database connection
  • exception during close file, input or output stream, network socket
  • InterrupedException during thread sleeping

try {
} catch (InterruptedException ex) {
    // log the exception as a warning

Even in those cases, it is better to log the exception for later investigation.

For more discussion on this issue, please see Item 65: Don't ignore exceptions in Chapter 9. Exceptions in Joshua Bloch’s Effective Java, Second Edition.

Exception Logging

Exception logging is for developers and system administrators to diagnosis the root problems and fix them. The logged information will be used offline. It is different from exception recovering and mitigating, which are done at runtime. Exceptions may be caused by incorrect user input or actions. There is almost nothing for developers and system administrators to do to fix the problems. Logging for such exceptions are not needed. Exceptions may be caused by limitations of system or network capacity, or problems of external systems. Logging such exceptions may help system administrators to have better idea about the capacity limitations and to plan for adding resources, or to communicate the problem with others who have control on the external systems. Exceptions may be caused by bugs in our own systems. Logging such exceptions is very helpful for developers to diagnose the root causes and fix them.

When log exception, try to include as much as possible information about the exceptions, include
  • Stack trace
  • Exception message
  • Data in the context – such as parameters to a certain method, state of a certain class.
  • All above for the nested exception, recursively.

Exception and Message to End User

Since every software application system is ultimately to server our end users, it is beneficial to think exceptions from the end users’ perspective. An exception means something is wrong. From an end user’s perspective, ultimately there are only two kinds of exceptions: user exceptions and system exceptions. A user exception is caused directly by a user with invalid input or incorrect action. End users are interested in this kind of exceptions because, if being informed, they can do something to correct them. For this kind of exceptions, the applications should provide enough information about the exceptions and hints to correct them to the end users, and should provide them the second chance to try. For on-line system, the system does not have to log such exceptions. For batch programs, such exceptions should be written into log files for end users, not for developers and system administrators, to read. System exceptions are not user-related, caused by bug in the application code, corrupted data in database, ill database management systems, ill network communication, or other external systems, etc. In short, they have nothing to do with what the end users have done and the end users can do nothing to correct them. All end users have to know about this kind of exceptions is that they occurred, no more details, particularly, not stack trace. Such exceptions have to be written into log files with as much as possible details for developers and system administrators to investigate. Stack trace of the exceptions should be included in the log files. Eventually, the thread in which a system error occurs should halt, with a brief message to the end user and a detail message in one or more log files for developers and system administrators.
It helps to have two top-level java exceptions for a system:  UserException and SystemException. Every thread started by our systems has to handle, at the top of the calling stack, these two kinds of exceptions if present.

Keep any information that might comprise security of the system out of message to end users.
It helps not to hard code message to end user in programming code. Instead, keep them in certain configuration files, such as Java properties file.

Don’t Create Custom Exception Unnecessarily

If possible, try to use exceptions in stand JDK, well-established libraries or framework, rather than create new custom exception. When a new custom exception is created, it takes time for other people to learn it. It also increases the size of our source code. Doing so only when it brings true benefits that existing exception classes cannot bring.

For more discussion on this issue, please see Item 60: Favor the use of standard exceptions in Chapter 9. Exceptions in Joshua Bloch’s Effective Java, Second Edition.

No comments: