Exception Handling - A Primer

Proper exception handling with applications is a critical and often misunderstood concept for many developers. In the Java world, I personally think the Eclipse IDE has done its utmost to propagate bad practice by generating the following when surrounding your method with a try catch block:

try {
    MyMethod();
} catch (Exception e) {
    e.printStackTrace();
}

printStackTrace() outputs the exception's full stack trace to the output window and while making the error information very easy to view when debugging, completely swallows the exception when running in production as the output window is not normally visible. While I'm singling out Java, the same type of construct can be seen in the .NET world.

Code like the above can lead to all sorts of problems, including but not limited to:

  • Users reporting that the submit button on a form is no longer working.
  • Specific areas of functionality in a complex system failing and this going unnoticed for a long period of time.
  • Developers having to debug the code, often with production data, to find out what the issue is.
  • Your error monitoring being blissfully unaware that anything is wrong with your application when the walls are falling down.

The following guidelines will help you build systems that are fault tolerant and allow for quick diagnosis of any underlying problems.

1. Always defer exception handling to the last possible point

Unless you have a specific reason, let exceptions bubble up to the default exception handling method of your application. This simplifies your code through defining your error handling code once and having it apply everywhere. Most applications have a method where you can access and log an unhandled exception such as the OnError hook within NancyFx or the UnhandledException event in a .NET console application, shown below:

static int Main(string[] args)
{
	AppDomain.CurrentDomain.UnhandledException += UnhandledException;
    // Run your code...
}

private static void UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    // Log error...
}

2. Only catch an exception if you plan on doing something with it

You should only catch an exception if you plan to handle the error there and then or if you plan to enrich the error in some way. For example, if you are writing a console application to alert users of an event via email, you may wish to catch the exception and wrap it with another specifying who the user was. For example:

public void SendEmails(List<User> users)
{
    foreach (var user in users)
    {
        try
        {
            // Code to send email
        }
        catch (Exception e)
        {
            throw new Exception(string.Format("Error sending email to user {0}", user.Id), e);
        }
    }
}

When doing this, be careful to include the original exception as an inner exception though the constructor. Without it, you would only be able to tell that an error had occurred for a given user rather than being able to track down the actual error.

3. Back-off and retry logic

Continuing with the example above, if you're writing a batch program to process a set of users, you can get into a situation where either a specific user causes an exception or a dependent service goes down (e.g an email endpoint). If you handle the exception at the last possible point, your program will crash and a portion of your list of users won't get processed.

In this situation, it's OK to catch and log the exception and then carry on to the next user. That is:

public void SendEmails(List<User> users)
{
    foreach (var user in users)
    {
        try
        {
            // Code to send email
        }
        catch (Exception e)
        {
            var specificException = new Exception(string.Format("Error sending email to user {0}", user.Id), e);
            LogException(specificException);
        }
    }
}

We can extend this functionality and make the program back off if it detects multiple errors in a short space of time. This, for example, will give you time to get your email endpoint back online and still process the majority of your users. How to do this deserves it's own post but in the meantime, I'll point you in the direction of a library that simplifies such a feature: Polly.

4. Handle the most specific exception possible

When handling exceptions, you should always handle the most specific exception possible rather than basing any logic on the message returned by the exception. For example, instead of:

catch (Exception ex) 
{
    if (ex.Message.Contains("Cannot insert duplicate")) 
    {
        // Do something
    }
    else
        throw
}

Rather use the following:

catch (DuplicateKeyException ex) {
    // Do something
}

Code like that of the first example is brittle and if the format of the error message ever changes, it could introduce a bug into your application.

5. If you need to swallow an exception, comment as to why

If you do come across a situation when you need to swallow an exception, it's polite to leave a detailed comment as to why you're doing it. Also, to avoid mistakenly swallowing an error that your application does care about, make sure the exception is as specific as possible.

try
{
    // Do some work
}
catch(SomeSpecificException e)
{
    // We don't care about this error because of xzy...
}

6. Throwing exceptions

Lastly, when re-throwing exceptions, make sure you do this:

catch (Exception ex) {
    // Code to handle exception
    throw;
}

rather than this:

catch (Exception ex) {
    // Code to handle exception
    throw ex;
}

The latter will destroy the original stack trace and make it look like the exception occurred in the catch block rather than where it actually occurred. This can lead to additional overhead when trying to track down an error, especially in a production environment.

That completes the basics of exception handling. The other articles in the Back to Basics series can be found here.

Show Comments