Don’t Break the Chained Exception
I was recently working with someone else’s code and grew frustrated at the way exceptions were handled. Of course, I was fixing their code, which is why I cared in the first place. I thought it of benefit to write down some simple rules about exception handling in the hopes of educating future developers in how not to write code that sucks.
Rule 1: Do Something
Never, ever, ever, ever, ever do the following…
try {
someObject.someMethod(stuff);
} catch (Exception ex) {
// do nothing
}
Never just eat an exception. If you are going through the trouble of writing the catch block in the first place, take a second and do something useful. You can do one of these (in descending order of preference)
- Handle it
- Re-throw it
- Log it
You should do at least one of these things.
Handle It
Handling the exception is the most preferable, of course. Do something the takes care of the error and makes it not a further problem. This is often hard, which is why it’s seldom done, but this is the most preferable of the things to do to an exception.
try {
someObject.someMethod(stuff);
} catch (Exception ex) {
// that must not exist, let me create it
someObject.createMethod(stuff)
}
This is the elegant solution.
Re-throw It (or Throw a New Exception)
This is the second most preferable solution. This should be done way more often than it is.
I say that because most people don’t realize that exceptions thrown by your services are a part of your services interface, just like the parameters and the return value. For example, let’s say you have a service with a doesUserStuff. Underneath, you are implementing this method using a Mauve™ SQL database.
In certain situations, Mauve, throws a MauveDatabaseError. If you decide not to catch this, then you end up with the following interface definition.
public User doesUserStuff(User user) throws MauveDatabaseError;
This is bad for two reasons.
- You have broken implementation hiding. Your client code now knows that your service code is using the Mauve
™
database. - Your client’s must then catch this error. If you later change the implementation to another database, this change ripples up to your client code. This is why we hide our implementation in the first place.
So, then, do this.
try {
someObject.someMethod(stuff);
} catch (MauveDatabaseError dataError) {
throw new MyDataError(dataError);
}
Now your client code doesn’t have to know about your underlying implementation and you are free to change it as much as you like.
Don’t Break the Chained Exception
While we’re on this topic, please note how the code above correctly chains the exception. You should never do the following. (At least without logging the original exception.)
try {
someObject.someMethod(stuff);
} catch (MauveDatabaseError dataError) {
throw new MyDataError(dataError.getMessage());
}
Now you’ve stripped off the valuable information about the code that errorred and made your job debugging this a lot harder. Chaining your exceptions preserve all information necessary for fixing the problem.
Log It
At the very minimum, if you don’t do anything else, you should log your exception. In this way, you’ve got the info in the log to debug it if you need it.
try {
someObject.someMethod(stuff);
} catch (Exception ex) {
LOG.error("Got exception", ex);
}
Summary
Exceptions should be handled, not eaten. Following the above directions will keep your teammates and your future self happy with you. Don’t code angry! Don’t Break the Chained Exception.