Effective Exception Handling

Do you want to be an exceptional programmer? If so, you should learn how to throw and handle exceptions properly.

Most modern programming languages support some form of exception handling. Exceptions are a construct that allow you to bypass the normal flow of the program and send an error to a higher level within the call stack.

Exceptions are a powerful tool. But like any tool, they can be abused. Let’s look at three ways to use exceptions properly.

Exceptions are Exceptional

By their very name, exceptions should be used in, well, exceptional circumstances. They are not intended to be used for “normal” program control flow. There are two reasons for this: They’re more expensive and tend to be more verbose.

Whenever your program raises an exception, several things happen. The context of the current call stack is collected, a new exception object is created, and the call stack is unwound until an appropriate exception handler is found. All of this takes time. Exceptions should be reserved for the edge cases, the parts of your code that aren’t expected to happen on a regular basis.

When used correctly, exceptions can help decouple error handling logic from the rest of your program. But when used for “normal” flow control, the verbosity of exception handling can make it harder to understand what your code is doing.

Consider the case of looking something up in the database. If the particular entity doesn’t exist, should you throw an exception (e.g. EntityNotFoundException) should you return null or some other “empty” result?

Consider these two code snippets:

1
2
3
4
5
6
7
MyObject myObj;
try {
myObj = myRepository.findById(id);
} catch (EntityNotFound err) {
myObj = new MyObject();
}
// do something with the object

versus

1
2
3
4
5
MyObject myObj = myRepository.findById(id);
if (myObj == null) {
myObj = new MyObject();
}
// do something with the object

Which do you find easier to read? I would argue that the second example is clearer most of the time.

One way to think about this is to use the 80/20 rule. If an error is likely to happen in the 80% use case, it should probably be handled with normal program control flow. Otherwise, if it’s in the other 20% of use cases, throwing an exception might be an appropriate solution.

Augment and Rethrow

When we do throw exceptions, it doesn’t do us any good if we don’t know what it means or how to debug why it’s being thrown in the first place. Often, exceptions can be fairly generic, especially when they’re thrown from a lower level library. Exceptions like IllegalArgumentException or SQLException don’t really provide much context as to what the program was doing at the time.

A great way to make things clearer is to wrap these exceptions with your own, more specific exception. Doing hasa few benefits:

For example, suppose we’re inserting a new record into our database. If the record exists, we might receive a DuplicateKeyException from our database library. To make this clearer, we might do the following:

1
2
3
4
5
6
7
User myUser = new User(...);
try {
db.persist(myUser);
return myUser;
} catch (DuplicateKeyException ex) {
throw new DuplicateUserException(myUser.getEmail(), ex);
}

This does a few things:

  1. Makes it clearer that the thing that’s duplicated is the User.
  2. Provides context as to which user we were trying to create by including the email address.
  3. Hides the implementation of the data store from the caller. If we decided to use another ORM or data store entirely, the callers wouldn’t need to change anything.

Notice that we still include the previous exception, as it is likely useful when the entire stack trace is logged. Most languages have a way of including the “cause” of an exception.

Speaking of logging exceptions…

Catch It When You Can Have To

Building upon our previous example, it is tempting to add some additional logging at the same time you’re rethrowing an exception.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
User insertUser(...) {
User myUser = new User(...);
try {
db.persist(myUser);
return myUser;
} catch (DuplicateKeyException ex) {
logger.error("User " + myUser.getEmail() + "already exists", ex);
throw new DuplicateUserException(myUser.getEmail(), ex);
}
}

// Yes, this example is a bit contrived, as it violates
// my first point about using exceptions for control flow

User user;
try {
user = insertUser(...);
} catch (DuplicateUserException ex) {
user = findUser(ex.getEmail());
}

The problem is that the caller may gracefully handle this error and we’ve now logged something that just adds noise to our logs. We may even have some monitoring that looks for “error” level log messages and alerts someone.

The key is to try and catch exceptions as far up in the stack as possible. If you can catch an exception and gracefully handle the error, great! Otherwise, you’re typically better off to let the error get as close to the UI layer as possible, where you can turn the error into a nicely formatted message shown to the user.

Take Control of Exceptions

Exceptions are a common feature of most modern programming languages. Learn to use them effectively and you’ll make your programs that much easier to read and understand. Use them only when appropriate, wrap and rethrow for clarity and only catch them at the last possible moment.

Question: What do you find to be the most difficult when using exceptions?