Exceptions Happen; Handle Them Well

Nothing can be said to be certain except for death and taxes — and that includes working software. Even the most bug-free software will experience exceptions. Work you’re doing could conflict with other work in the system. You could run out of space to write files. The cloud service you depend on could be out of commission.

Fortunately, we often have exception handling to help us deal with these issues. Unfortunately, it’s pretty easy to create other problems while using it.

Most server-side systems I’ve used keep exceptions out of sight. You can write your implementations and not sweat the bad stuff. Out of the box, most setups catch the majority of unexpected failures, log them, and inform API consumers that some unspecified bad thing happened. So far, so good.

This isn’t necessarily a bad place to be in. You’re generally not leaking potentially sensitive information outside your system. Nobody expects that something happened when it really didn’t.

But you’ll probably need to do better than that, at least in some cases. The logs you get when things go awry may not be helpful. You may find, as you’re developing, that you expect certain kinds of failure modes that you need to handle gracefully.

Be Conservative in What You Except

Postel’s Law tells us to “be conservative in what you do, be liberal in what you accept from others.”

Postel’s Law can help us write robust software, but we have to be careful about how we wield it. Being liberal in what you accept from other systems doesn’t mean taking anything that comes your way. Down that road lies security issues and all sorts of lesser bugs.

Instead, judge just how liberal you need to be in what you accept. Then, explicitly handle each kind of case you need to, and — this is critical — do not claim to handle what you do not.

In practice, claiming to handle things you actually don’t looks like this:


try {
    doSomething();
} catch (Exception e) {
    log.error("something bad happened", e);
}

The good news is that, if you have logging set up properly, this will put bright red error logs that should show up in any monitoring system worth its salt. The bad news is that your code will proceed happily as if nothing at all went wrong.

To avoid this situation, be explicit about what you’re looking to catch, and only catch it if you’re going to do something useful with the knowledge you gained.


try {
    doSomething();
} catch(RequestedWidgetNotFound e) {
    // return a 404 to the caller
} catch(SqlException e) {
    if (isADeadLock(e)) {
        // immediately retry the transaction (watch the retry count!)
    } else {
        throw e; // let the framework handle it
    }
} catch(Throttled e) {
    // queue a retry for later
} 

Here we have a few things we expect we might see failure from. We’ll do something smart with each one. And — this is key — if something else failed, we let that exception bubble up.

A side note: the above example catches a few common and generic things. It’s good to implement handlers like these in a common place, using facilities from your framework if you can.

Sometimes it’s Okay to Catch Everything

Catching Exception is a code smell, but that doesn’t mean it’s always wrong to do so.

First of all, if you’re writing a framework that should catch any failure and handle it in the same way, it makes a lot of sense to catch everything once you’ve done any specific expected exception processing.

Another reason you might want to have a broad catch is if you need to do something with an exception, then let it bubble up. A common pattern is if you want to specifically log the place where you experienced a failure:


try {
    doSomethingVerySpecific()
} catch (Exception e) {
    log.error("failed doing this very specific thing", e);
    throw e;
}

Exceptions to the Rule

Unfortunately, not all ways that your code can fail will throw exceptions in the first place. Other times, exceptions may be thrown deep inside your implementations, but they’re hopelessly generic to the point you have no hope of figuring out what went wrong.

Don’t be afraid to translate a failed check or a generic exception into something more specific if you need to handle that failure a specific way. For example, many Java frameworks I’ve worked with will throw IllegalArgumentException if a method doesn’t like the value of one of its arguments. This is useful to know at the point of failure, but by the time you bubble up, say, an endpoint method, you have no way to know what the problem was.

You can — and should — define your own exceptions in this case if you have a specific way to handle that problem. If you aren’t going to handle it specifically, let it go. (Hopefully, your framework is going to save a traceback for you, so you can figure out where this happened.)

Another use for your own custom exceptions is to keep specific concerns in their layer. For example, only a web framework controller should be thinking about throwing a specific HTTP error code. Have your business logic or repository layers throw exceptions that make sense for them, and adapt them in your web layer to the appropriate HTTP responses.

Think All Your Exceptions Through

The above are a few guidelines I go by when I write and review code to help make it robust in the face of exceptional conditions. But to really succeed at reliable, diagnosable code, you should always be paying attention to the intersection of how you expect it to fail and tools you have to recover from (or, at the very least, log) those failures.

Taking the time to understand this won’t save your code from experiencing exceptional conditions, but it will help save you and your users when they happen.