Java Runtime Exception Policy

The key tenet of this exception policy is that exceptions should be designed with an eye toward what the catch clause would likely do with the exception. The three main cases are:

Code Exceptions

These are exceptions where it is expected that some calling code may be able to completely handle the exception, without involvement of any user. In other words, the exception is of an alternate way of returning a value. There is a reasonable chance that calling code (maybe a couple levels up) will be able to catch the exception and either try again or try some other approach to accomplishing its job. Note that there may be no way of knowing whether the caller will be able to figure out a different approach to handling the situation. This is especially true in reusable utility code. In these cases, the exceptions should be considered to be code exceptions. The code that handles the exception might just turn it into a different kind of exception.

Implications

In general, code exceptions should be checked exceptions. They should be named based on what happened, rather than based on who is throwing the exception. If the exception is well named, it should be possible for the exception to be present on signatures at several levels of a call stack and still make sense (e.g. ServiceUnavailableException).

There are some cases where code exceptions should not be checked exceptions. If code cannot reasonably be expected to recover from an exception, it should be unchecked, Also, iIf a large fraction of the methods in the code would need to declare the exception, then its declaration doesn't add much value and so it should be a RuntimeException so it doesn't need to be declared. One example of this kind of exception might be a RetryException. This exception might occur on some kind of resource conflict where retrying the transaction is likely to solve it. Since it is solved without human involvement it is still a "code exception".

User Exceptions

These are exceptions that signal a problem that will be handled by a person, so the most important component of the exception is the message, rather than the type of the exception. Unfortunately, the code that throws the original exception often will not have enough information to give a meaningful message to the user that has all the necessary context. The typical "user" in this situation is an administrator, where a stack traceback wouldn't be very helpful. Because of this, it is important that code be littered with try/catch blocks that do no more than add context to the exception message and then rethrow.

In a previous project this was done by having a base UserException class that had an array of messages, rather than just one message. For example, code that parses an SCA subsystem file might have a rethrow that just adds "While parsing the xyz subsystem file". That is a message that could not be generated by the code that discovered the problem (say an XML parsing problem), so a combination of the original message (e.g. "Missing end tag") and the higher level message ("while parsing the xyz subsystem file") are both necessary for know what happened. Naturally it can be any number of levels deep.

The handling code for a user exception will somehow notify a user of the message and then possibly go on. There should be different kinds of exceptions when there need to be different ways of handling of the message or different ways to continue. Different ways to report the error: In a server, user exceptions can often be divided according to fault:

If the problem is the fault of the client code, then the message needs to be reported back to the client code in a format appropriate for the client. If the problem is the fault of the server code or configuration, then only a vague "I've got a problem here" message should be sent to the client and the real exception message should be logged and/or sent to an administrator. Because of the two different ways of handling the problem, there should be different exception types. For example, ClientException could be used for exceptions that signal problems that are the client's fault.

The remaining user exceptions are typically problems with configuration or the environment. Some of them will be severe enough that the entire application needs to be brought down, while others could be handled by just logging the problem and going on. This difference implies that there needs to be a different exception type. Advanced Scenario: In the case of session-scoped services, the problem is likely to require that the instance of the service be put into an error state (like paused). This is because subsequent messages for the service have been sent on the assumption that the previous message actually gets processed. If some configuration error prevents a session-scoped service from handling a single message, all future (async) messages for that service instance should be queued up so they can be processed once the problem has been solved.

Assertion Exceptions

Assertion exceptions are exceptions that result from a bug in Tuscany and as such are also intended to be solved by humans, but in this case the humans are us --are the developers of the SCA runtime. In these cases the message isn't nearly as important, since the stack traceback provides valuable context. If an assertion exception occurs little can be known about the state of the server. If we wanted to be safe we would say that assertion exceptions always bring down the entire server. However, we could play it a little looser and say that assertion exceptions only bring down the application in which they are discovered.

Guidelines

The following are a set of guidelines based on the above exception philosophy:

1. Checked vs. unchecked exceptions

Unchecked exceptions should be used when an error condition is not recoverable. Checked exceptions thrown by third party libraries that are not recoverable should be wrapped in unchecked exceptions rather than being propagated up the call stack. For example, an IOException raised when reading a file might be wrapped in an unchecked LoadException containing the name of the file. Unchecked must always be Javadoced and declared in the throws clause of a method.

2. Assertion exceptions should use the standard JDK assert facilities

3. Any exception thrown to user code must extend the appropriate Exception as defined by the specification. This will typically be a runtime Exception.

4. No other Exceptions should be thrown to user code. Each user API method should catch any internal exceptions and wrap them in the applicable Exception defined by the specification. Internal exceptions must ultimately extend either TuscanyException or TuscanyRuntimeException.

4. When possible, create clear package exception hierarchies

In most cases, packages should have a clear exception hierarchy with abstract root checked and unchecked exceptions which more specific concrete exceptions extend. Declaring the root package exceptions abstract avoids code throwing exceptions which are too general. Creating an exception hierarchy allows client code using a particular package to choose the level of exception handling granularity (which in turn simplifies the client code by avoiding unwieldy try..catch clauses).

5. Preserve all stack trace information and the original exception

Exceptions must always preserve the stack trace and original exception except under special circumstances. When wrapping exceptions to propagate, never modify the stack trace and always include the caught exception as the cause.

6. Only include local information pertinent to the failure

For I18N, contextual information stored in the Exception should not be localized. It should comprise only data pertaining to the cause, such as the name of the artifact as above, or a key that can be used by the top level exception handler. This is needed because the locale used to render the exception may be completely different from the locale used by the code raising the exception. For example, an exception may be thrown on a system whose default locale is German, logged to the system log in English but displayed to the end user in French, Japanese, whatever their native language is.

7. For exceptions that require contextual information from various code layers, either wrap exceptions or create exceptions that can accept additional context as they are propagated up the call stack.

If a failure requires information from multiple levels, e.g. “there was an error setting property X on component Y in module Z” do one of the following. If the initial exception should be wrapped as it is propagated (e.g. the exception occurs at a library boundary), add additional context information in the wrapping exception(s). If the initial exception can be propagated, include methods for adding additional context information as the exception is rethrown up the stack. For example, the previous failure scenario could result in the following exception handling strategy:

8. getMessage() must return unformatted context info. If the Exception contains multiple context fields they should be surrounded in square brackets and separated by commas, e.g. "[ property X, component Y, module Z ]"

9. Do not override the behaviour of Throwable.toString() and Throwable.printStackTrace()

10. The java.lang.Exception base class is Serializable so all subclasses must provide a serial UID. Any context fields must be Serializable and should be defined in the base java namespace for JDK1.4.

11. Exceptions that wrap other Exceptions should ensure that any wrapped Exception can be deserialized in a client environment. This may require providing a custom writeObject method to extract any context information from the wrapped Exception during serialization; at a minimum the message should be preserved.