Best Practices for Exception Handling and Logging
The Nature of Exceptions
Broadly speaking, there are three different situations that cause exceptions to be thrown:
Exceptions due to programming errors: In this category, exceptions are generated due to programming errors (e.g., NullPointerException and IllegalArgumentException). The client code usually cannot do anything about programming errors.
Exceptions due to client code errors: Client code attempts something not allowed by the API, and thereby violates its contract. The client can take some alternative course of action, if there is useful information provided in the exception. For example: an exception is thrown while parsing an XML document that is not well-formed. The exception contains useful information about the location in the XML document that causes the problem. The client can use this information to take recovery steps.
Exceptions due to resource failures: Exceptions that get generated when resources fail. For example: the system runs out of memory or a network connection fails. The client's response to resource failures is context-driven. The client can retry the operation after some time or just log the resource failure and bring the application to a halt.
Best Practices for Exception Handling
1. When deciding on checked exceptions vs. unchecked exceptions, ask yourself, "What action can the client code take when the exception occurs?"
If the client can take some alternate action to recover from the exception, make it a checked exception. If the client cannot do anything useful, then make the exception unchecked. By useful, I mean taking steps to recover from the exception and not just logging the exception.
Moreover, prefer unchecked exceptions for all programming errors: unchecked exceptions have the benefit of not forcing the client API to explicitly deal with them. They propagate to where you want to catch them, or they go all the way out and get reported. The Java API has many unchecked exceptions, such as NullPointerException, IllegalArgumentException, and IllegalStateException. I prefer working with standard exceptions provided in Java rather than creating my own. They make my code easy to understand and avoid increasing the memory footprint of code.
2. Preserve encapsulation.
Never let implementation-specific checked exceptions escalate to the higher layers. For example, do not propagate SQLException from data access code to the business objects layer. Business objects layer do not need to know about SQLException. You have two options:
1) Convert SQLException into another checked exception, if the client code is expected to recuperate from the exception.
2) Convert SQLException into an unchecked exception, if the client code cannot do anything about it.
3. Try not to create new custom exceptions if they do not have useful information for client code.
4. Do not use your base exception class for "unkown" exception cases.
actually a follow-up from the first advice above. If you model your own exception hierarchy, you will typically have an exception base class (eg. MyAPIException) and several specific ones that inherit from that one (eg. MyAPIPathNotFoundException). Now it is tempting to throw the base exception class whenever you don't really know what else to throw, because the error case is not clear or very seldom. It's probably a Fault and thus you would start mixing it with your Contingency exception class hierarchy, which is obviously a bad thing.
One of the advantages of an exception base class is that the client has the choice to catch the base class if he does not want to handle the specific cases (although that's probably not the most robust code). But if that exception is also thrown in faulty situations, the client can no longer make a distinction between one-of-those-contingency-cases and all-those-unexcpected-fault-cases. And it obviously brakes your explicit exception design: there are those specific error cases you state and let the client know about, but then there is this generic exception thrown where the client cannot know what it means and is not able to handle it as a consequence.
5. When wrapping or logging exceptions, add your specific data to the message.
6. Don't throw exceptions in methods that are likely to be used for the exception handling itself.
This follows straight from the two previous advices: if you throw or log exceptions, you are typically in exception handling code, because you wrap a lower-level exception. If you add dynamic data to your exceptions, you might access methods from the underlying API. But if those methods throw exceptions, your code becomes ugly.
7. Document exceptions.
Best Practices for Using Exceptions
1. Always clean up after yourself
If you are using resources like database connections or network connections, make sure you clean them up. If the API you are invoking uses only unchecked exceptions, you should still clean up resources after use, with try - finally blocks.
2. Never use exceptions for flow control
Generating stack traces is expensive and the value of a stack trace is in debugging. In a flow-control situation, the stack trace would be ignored, since the client just wants to know how to proceed.
3. Do not suppress or ignore exceptions
When a method from an API throws a checked exception, it is trying to tell you that you should take some counter action. If the checked exception does not make sense to you, do not hesitate to convert it into an unchecked exception and throw it again, but do not ignore it by catching it with {} and then continue as if nothing had happened.
4. Do not catch top-level exceptions
5. Log exceptions just once
Logging the same exception stack trace more than once can confuse the programmer examining the stack trace about the original source of exception. So just log it once.
Logging
When your code encounters an exception, it must either handle it, let it bubble up, wrap it, or log it. If your code can programmatically handle an exception (e.g., retry in the case of a network failure), then it should. If it can't, it should generally either let it bubble up (for unchecked exceptions) or wrap it (for checked exceptions). However, it is ultimately going to be someone's responsibility to log the fact that this exception occurred if nobody in the calling stack was able to handle it programmatically. This code should typically live as high in the execution stack as it can. Some examples are the onMessage() method of an MDB, and the main() method of a class. Once you catch the exception, you should log it appropriately.
The JDK has a java.util.logging package built in, although the Log4j project from Apache continues to be a commonly-used alternative. Apache also offers the Commons Logging project, which acts as a thin layer that allows you to swap out different logging implementations underneath in a pluggable fashion. All of these logging frameworks that I've mentioned have basically equivalent levels:
1) FATAL: Should be used in extreme cases, where immediate attention is needed. This level can be useful to trigger a support engineer's pager.
2) ERROR: Indicates a bug, or a general error condition, but not necessarily one that brings the system to a halt. This level can be useful to trigger email to an alerts list, where it can be filed as a bug by a support engineer.
3) WARN: Not necessarily a bug, but something someone will probably want to know about. If someone is reading a log file, they will typically want to see any warnings that arise.
4) INFO: Used for basic, high-level diagnostic information. Most often good to stick immediately before and after relatively long-running sections of code to answer the question "What is the app doing?" Messages at this level should avoid being very chatty.
5) DEBUG: Used for low-level debugging assistance.
If you are using commons-logging or Log4j, watch out for a common gotcha. The error, warn, info, and debug methods are overloaded with one version that takes only a message parameter, and one that also takes a Throwable as the second parameter. Make sure that if you are trying to log the fact that an exception was thrown, you pass both a message and the exception. If you call the version that accepts a single parameter, and pass it the exception, it hides the stack trace of the exception.
When calling log.debug(), it's good practice to always surround the call with a check for log.isDebugEnabled(). This is purely for optimization. It's simply a good habit to get into, and once you do it for a few days, it will just become automatic.
Do not use System.out or System.err. You should always use a logger. Loggers are extremely configurable and flexible, and each appender can decide which level of severity it wants to report/act on, on a package-by-package basis. Printing a message to System.out is just sloppy and generally unforgivable.
No comments:
Post a Comment