On every project I’ve worked on, one of the first things I have to do is work out why an exception was thrown. Usually, that first error message doesn’t contain enough information to help me solve the problem. At this point I congratulate the previous developer of the system on getting so far with such sloppy error handling.
After congratulating myself, I move on to reviewing the project’s approach to exception handling. I like to do this globally if possible, to ensure that the approach is consistent throughout the project, and so that I can have reasonably expect to see the information I need next time something goes wrong.
Good exception handling and reporting is critical to the success of any software project. Since the bulk of a project’s lifetime is spent in bug fixing and maintenance, good error reporting can make this phase of a project much less time consuming that it might be. With a stack trace, I can immediately tell where the fault occurred, and who was calling that code. In most cases, that’s enough information to understand the problem.
I’ve completely refactored the exception handling of a couple of medium-sized projects; in doing so, I’ve observed a few rules of thumb make this process a breeze if followed. I’ll try to summarize them here:
Don’t swallow nested exceptions
Change this:
try { ... }
catch (Foo e) { throw Bar() }
to this:
try { ... }
catch (Foo e) { throw Bar(e) }
The first exception thrown is always the most important for software developer; it provides information about the underlying cause and location of a failure. When you discard it this way, determining what happened is next to impossible.
In case you’re wondering, the following is not a good substitute:
try { ... }
catch (Foo e) { throw Bar(e.getMessage()) }
Tempting, but count yourself lucky if you get any useful information this way; you’ll probably get a vague message, or perhaps nothing at all. And you just lost the most useful piece of information: the stack trace of the underlying error.
Don’t add messages to exceptions when you have nothing to say
Change this:
try { ... }
catch (Foo e) { throw Bar("Error", e) }
to this:
try { ... }
catch (Foo e) { throw Bar(e) }
If you are simply converting an exception from one type to another, there may be no need to add information. You might be changing a caught exception into a runtime exception, or into an application or library specific exception type. In such cases, there’s usually no reason to add more information; the exception itself can provide this information.
Doing this has two benefits: you don’t have to read useless information when something goes wrong, and you don’t have to spend time thinking of something useless to say.
Don’t catch exceptions if you don’t need to
Change this:
try { foo() }
catch (Foo e) { throw Foo(e) }
to this:
foo()
If you’re going to throw an exception of the same type as that you caught, and you aren’t adding any useful information… don’t handle the exception at all.
Use checked exceptions sparingly
Change this:
g() throws Gee { throw Gee() }
f() throws Foo {
try { g() }
catch (Gee e) { throw Foo(e) }
}
to this:
g() throws RuntimeBar { throw RuntimeBar() }
f() { g() }
A fatal error is a fatal error is a fatal error. When you force the caller of a function to handle a problem they can’t solve, all you’re doing is making them churn out pointless exception handling code.
If there’s a significant chance that when you throw an exception, the caller is not going to be able to continue, make sure that it’s a runtime exception. That way, most callers won’t have to write redundant exception handling code; they’ll still have the option, but they won’t be forced to.
Checked exceptions are less useful than you might think. When you call a method that declares and exception, this does not mean that the method cannot throw anythig else, just that it can only throw checked exceptions of that type. Even that rule can be broken. And of course, that method is free to throw all manner of unchecked exceptions. Do you trust it not to?
What this means is that if you really want to catch everything that a method can throw, you always have to catch Throwable.
Use a single exception type for all application code
Change this:
g() throws Gee { ... }
f() throws Foo {
try { g() }
catch (Gee e) { throw Foo(e) }
}
to this:
g() throws Bar { ... }
f() throws Bar {
g()
}
This is great way to eliminate large amounts of unnecessary exception handling code. If you really need to have differentiated exceptions, you can either add an identifying property to the exception class, or use inheritance, so that callers can just handle the base exception class if desired.
Reuse existing exception types where appropriate
Change this:
doSomethingDatabasey() throws DatabaseException {
try {
... // jdbc code
} catch (SQLException e) {
throw new DatabaseException(e)
}
}
f() throws Foo {
try {
... // jdbc code
doSomethingDatabasey()
} catch (SQLException e) {
throw Foo(e)
} catch (DatabaseException f) {
throw Foo(f)
}
}
to this:
doSomethingDatabasey() throws SQLException {
... // jdbc code
}
f() throws Foo {
try {
... // jdbc code
doSomethingDatabasey()
} catch (SQLException e) {
throw Foo(e)
}
}
If you’re augmenting or reusing an existing library or API, reuse it’s exception types. This lets you substitute and mix calls from both interfaces without having to change or add new exception handling code.
Java built-in exception reporting is inadequate – don’t use it
Change this:
main() {
doSomething()
}
to this:
main() {
try {
doSomething()
} catch (Throwable e) {
reportException() // you'll need to write this...
}
}
Java’s built in exception handling does a pretty poor job of distilling the information contained in an exception into a form that is useful for debugging. This is what happens when you call getStackTrace() or printStackTrace(), or when an exception is never caught and propagates out of main(). This should never happen, which means you should be sure to catch all exceptions in main; the only way to do this is to catch Throwable.
So what do we want from our exception reports? Stack traces, messages, nested exception information, bean properties… That’s a topic for another post!
Provide stack traces
While we don’t want to scare users by showing them a full stack trace, it is essential to be able to get this information for troubleshooting purposes. Depending on the type of application, you approach may vary.
Logging: Ensuring that all exceptions are logged in detail is essential in all applications. By logging stack traces, you can show basic error information that might enable a user to resolve the issue, while still having the ability to get a stack trace if necessary. Exceptions that occur while logging should be sent to stderr.
Verbose flag: An easily flipped switch to ask for verbose error output can work adequately in many cases. By default, only display the exception message. If this is clear, it might be enough for the user to resolve the problem. If the problem is a bug, rather than a configuration issue or temporary system outage, setting the verbose flag should provide full information to the development team. The downside of this approach is that is requires the bug to be easily reproducible, as a stack trace cannot be obtained for the original exception.
Details button: In a GUI or web interface, you can allow users to drill down to a stack trace if necessary, by providing a button for accessing this detail. Typically though, you can’t expect a user to glean anything from a stack trace, so perhaps a ’send to support’ link would be more useful here.
Provide class loader information
In complex environments, such as an application container, dependencies are often problematic. When reporting and exception, it can be very useful to provide as much information as you can about the class loader hierarchy, and where each of those class loaders locates the classes involved in the stack trace. In my experience this can save a lot of time when troubleshooting dependency problems.