I’ve written quite a few articles on my blog about the use of exceptions vs Result class. I think the best one that summarizes the differences is this: Error handling: Exception or Result?

The main idea is that all the differences between various types of failure can be boiled down into the following two categories:

  • Errors we know how to deal with (expected errors),

  • Errors we don’t know how to deal with (unexpected errors).

You should use a Result class for the first type of failure, and you should use exceptions for the second one.

1. Example use case of the Result class

Here’s how you would usually use the Result class:

public async Task<Result> SendNotification(string endpointUrl)
{
    HttpResponseMessage message;
    try
    {
        message = await _httpClient.PostAsync(endpointUrl);
    }
    catch (HttpRequestException ex)
    {
        return Result.Failure("Unable to send notification");
    }
    string response = await message.Content.ReadAsStringAsync();
           
    /* do something with the response */
 
    return Result.Success();
}

// Somewhere upper the call stack
Result result = await SendNotification(endpointUrl);
if (result.IsSuccess)
{
    MarkNotificationAsSent();
}
else
{
    MarkNotificationAsNotSent();
}

The error is returned by this line:

return Result.Failure("Unable to send notification");

We could also use a strongly-typed error instead of a string:

return Result.Failure(Errors.Notifications.UnableToSend());

Notice that the error we are converting into a Result is an expected one. The code above knows what to do if the POST request fails: the failure gets persisted to the database for later examination.

2. Stack traces in the Result class?

There’s a question about the Result class that I see quite often:

You use simple strings or enum values to differentiate known errors. The idea is that we catch an exception at a low level and convert it to Result with some error which is represented by a string / number / enum value.

But when we doing this, we lose the information about the exception and its stack trace. It may be hard to figure out the source of the problem just from the error code (or error description).

So my question is this: would it be a good solution to put the source exception or its stack trace in the Result class as a property in order to retain all information about the exception?

Indeed, in the above example, we could do something like this:

HttpResponseMessage message;
try
{
    message = await _httpClient.PostAsync(endpointUrl);
}
catch (HttpRequestException ex)
{
    // Passing the ex instance to the result
    return Result.Failure("Unable to send notification", ex);
}

And use that exception when processing the failed result instance. Would it be a good idea to do so?

No, it wouldn’t.

Saving the exception or its stack trace to Result defeats the purpose of converting exceptions into Result in the first place.

Think about it. Why would you need the stack trace? The only reason is to log it for further review, which means you are catching an exception you don’t know how to deal with.

As we discussed earlier, Result is only for exceptions you know what to do about. Those are exceptions that you already studied in full and know how to handle gracefully.

If that’s not the case, then you are using the Result class incorrectly: you are using it to handle unexpected exceptions.

For instance, let’s say that in the above example, we should only mark the notification as unsent if the HTTP request returns a 500. If the response is a 400 or 404 error, it means there’s something terribly wrong with our request; something that we can’t recover from — a bug. No matter how many times we retry such a request, the result is going to be the same.

We can’t do anything about such unrecoverable errors at runtime. They require a programmer intervention, i.e a bug fix. What we must do in the meantime is attract as much attention to this error as possible.

And what can possibly help us such attraction?

That’s right, the fail fast principle: throwing an exception such that it stops the current operation entirely. We should let the exception propagate to the top-most level of the call stack, where it will be logged and converted into a 500 response.

3. Conclusion

In other words, if you feel the need to put the exception or its stack trace to Result, it means you are not differentiating between expected and unexpected errors in code.

In the above example, we should narrow down the error handling process, like this:

HttpResponseMessage message;
try
{
    message = await _httpClient.PostAsync(endpointUrl);
}
catch (HttpRequestException ex)
{
    // Only process (retry) 5xx errors
    if ((int)ex.StatusCode >= 500)
        return Result.Failure("Unable to send notification");

    // 4xx errors mean we have a bug in our code
    throw new Exception("Incorrect request", ex);
}

Notice that we don’t log the exception here. That’s once again because we don’t need to log excepted exceptions (we’ve already studied them in full), while unexpected ones are logged by the top-most error handler (which also converts them into 500 responses).

This is the ideal situation. But if you aren’t sure you’ve narrowed down your exception handling precisely, you may log the exception here as well:

HttpResponseMessage message;
try
{
    message = await _httpClient.PostAsync(endpointUrl);
}
catch (HttpRequestException ex)
{
    // Logging the exception just in case
    _logger.Log(ex);

    // Only process (retry) 5xx errors
    if ((int)ex.StatusCode >= 500)
        return Result.Failure("Unable to send notification");

    // 4xx errors mean we have a bug in our code
    throw new Exception("Incorrect request", ex);
}

This is just in case you accidentally covert an unexpected error into Result, so that you can review such occasions later.

Ideally, you shouldn’t keep such logging forever, though. After some time, when you see there are no issues with your differentiating logic, it’s best to remove the logger so that it doesn’t clutter your code (and logs).

--Vlad

https://enterprisecraftsmanship.com


Enjoy this message? Here are more things you might like:

Workshops — I offer a 2-day workshop for organizations on Domain-Driven Design and Unit Testing. Reply to this email to discuss.

Unit Testing Principles, Patterns and Practices — A book for people who already have some experience with unit testing and want to bring their skills to the next level.
Learn more »

My Pluralsight courses — The topics include Unit Testing, Domain-Driven Design, and more.
Learn more »