Make the exceptions thrown by your methods more informative and useful by including more information with each exception. Do this by delegating the creation of exceptions instances to helper methods that focus on one task.

Throwing exceptions is the established C# idiom for when your methods (or instances) encounter an exceptional situation that they’re not prepared to handle.

Consider this simple method, based on one that I’ve had in my toolbox for quite some time, used to convert a string value into a specific type:

public static bool TryConvert<T>(this string value, out T result)
{
    // Use a TypeConverter if one is available
    var converter = TypeDescriptor.GetConverter(typeof(T));
    if (converter == null || !converter.CanConvertFrom(typeof(string)))
    {
        result = default;
        return false;
    }

    try
    {
        result = (T)converter.ConvertFromString(value);
        return true;
    }
    catch (Exception)
    {
        throw new InvalidOperationException();
    }
}

This method lets you do safe type conversions like this:

if (value.TryConvert<Color>(out var result)) 
{
    // do something with result
}

The problem with TryConvert<T>() as written above is that the exception it throws is completely uninformative. It doesn’t really tell you anything other than something went wrong, and you don’t even know where unless you capture the stack trace.

(I’ve also had someone point out that catching Exception is generally considered a bad idea in this method. Unfortunately, some type converters throw Exception directly - check out BaseNumberConverter for example.)

The first thing we can do to improve the situation is to include both an informative message and the original exception. The result looks something like this (showing only the relevant fragment to save space):

catch (Exception ex)
{
    var failureMessage = 
        $"Failed to convert \"{value}\" to {typeof(T).Name} ({ex.Message})";
    throw new InvalidOperationException(failureMessage, ex);
}

Now the exception tells us what went wrong - but, it’s still not great.

What happens if/when we’re debugging and we want to pull out the original value in order to create a new test or debug the issue in more depth? Having to select and copy just the right characters out of the message is error-prone and tedious.

We can use the Data dictionary present on every exception to carry additional data, as follows:

catch (Exception ex)
{
    var failureMessage = 
        $"Failed to convert \"{value}\" to {typeof(T).Name} ({ex.Message})";
    var exception = new InvalidOperationException(failureMessage, ex);
    exception.Data["value"] = value;
    exception.Data["desiredType"] = typeof(T);
    throw exception;
}

The downside of this is that our original method has grown substantially in size; let’s extract the creation of the exception into a helper method:

private InvalidOperationException TypeConversionError<T>(
    string value, Exception exception)
{
    var failureMessage = 
        $"Failed to convert \"{value}\" to {typeof(T).Name} ({exception.Message})";
    var result = new InvalidOperationException(failureMessage, exception);
    result.Data[nameof(value)] = value;
    result.Data["desiredType"] = typeof(T);
    return exception;
}

Observe that we don’t throw the exception within the helper, we return it to be thrown by our original method, which is now substantially shorter and easier to read.

try
{
    result = (T)converter.ConvertFromString(value);
    return true;
}
catch (Exception ex)
{
    throw TypeConversionError<T>(value, ex);
}

Isolating the code to create the exception within the helper class gives you a distinct space to focus on what that exception should do. As I’ve used this technique through my own code, I’ve found that this leads to code that’s easier to analyze when it fails.

Comments

blog comments powered by Disqus
Next Post
Sharpen The Saw #21  27 Nov 2017
Prior Post
Sharpen The Saw #20  20 Nov 2017
Related Posts
Browsers and WSL  31 Mar 2024
Factory methods and functions  05 Mar 2023
Using Constructors  27 Feb 2023
An Inconvenient API  18 Feb 2023
Method Archetypes  11 Sep 2022
A bash puzzle, solved  02 Jul 2022
A bash puzzle  25 Jun 2022
Improve your troubleshooting by aggregating errors  11 Jun 2022
Improve your troubleshooting by wrapping errors  28 May 2022
Keep your promises  14 May 2022
Archives
November 2017
2017