Throw Rich Exceptions that are thoroughly informative to allow easy fault diagnosis. This is the tenth in a series of posts on Code Gardening - the practice of making small improvements as you see them while you are working.

System failures are inevitable. When something does go wrong, your code has one final responsibility - to leave enough diagnostic information to allow someone to work out what happened.

Let’s return to the FindMandatoryCourseByCode() method that we last discussed in the post Boolean Parameters are Evil.

public Course FindMandatoryCourseByCode(string code)
{
    Require.MustNotBeBlank(code, nameof(code));

    var result = FindOptionalCourseByCode(code);
    if (result != null)
    {
        return result;
    }

    throw new InvalidOperationException("Course not found");
}

When this exception is thrown, there’s no context given to explain what went wrong - we don’t even get told which course code couldn’t be found. (It’s better than some code, however - at least it’s throwing a useful exception type and not just Exception itself.)

Our first improvement is to make the message more informative so that the actual course code is visible as a diagnostic when required.

var message = $"Course '{code}' not found";
throw new InvalidOperationException(message);

The course code itself is surrounded by quote (‘) characters so that any leading or trailing whitespace is clearly visible. They also serves to highlight an empty code if one is used.

There is more we can do, however. Every .NET exception has Data, a dictionary of name/value pairs that is carried along, providing a built-in mechanism for including extra details.

In our case, providing the actual course code opens up other possibilities for handling or reporting on the exception.

var message = $"Course '{code}' not found";
var exception = new InvalidOperationException(message);
exception.Data["courseCode"] = code;
throw exception;

At the very least, your outer logging layer should capture the content of this dictionary to ensure the information is not lost.

Aside: For almost every exception the Data property will be automatically initialized, so you can rely on it being present when you’re creating the exception itself. However, I’ve seen at least one authorative source indicate that some system exceptions (such as OutOfMemoryException) can show up with Data set to null. Just to be safe, always check to see if Data is assigned before doing anything with an exception you’ve caught.

To prevent the exception code from overwhelming the original method, it is often useful to extract it into a private factory method:

private InvalidOperationException 
    CourseNotFoundException(string code)
{
    var message = $"Course '{code}' not found";
    var exception = new InvalidOperationException(message);
    exception.Data["code"] = code;
    return exception;
}

This can easily be thrown from wherever required:

throw CourseNotFoundException(code);

Note that we don’t throw the exception within the factory method - instead we throw it from the same place we always used, so that the stack trace takes us directly to the right place.

Idiomatically this can look a little odd because we are so used to seeing new immediately after throw.

As a last point, note that this can be taken another step, adding additional information to other exceptions as they pass through:

try
{
    // ...
}
catch (Exception ex)
{
    if (ex.Data != null)
    {
        ex.Data["user"] = currentUser;
        ex.Data["userName"] = currentUser.Name;
        ex.Data["subscription"] = currentUser.Subscription;
    }

    throw;
}

Comments

blog comments powered by Disqus