Error handling: Exception or Result?

By Vladimir Khorikov

In this post, we’ll look at some practical examples of error handling. We will see whether it is better to use exceptions or the Result class to deal with errors.

Error handling: Exception or Result?

I mentioned previously in this blog and also in my Functional C# Pluralsight course that programmers tend to overuse and even abuse exceptions. It’s common to see situations where some code encounters a programmatic failure, throws an exception, and then another piece of code handles that exception some levels upper the call stack:

public async Task SendNotification(string endpointUrl)

{

    try

    {

        HttpResponseMessage message = await _httpClient.PostAsync(endpointUrl);

        string response = await message.Content.ReadAsStringAsync();

        /* do something with the response */

    }

    catch (Exception ex)

    {

        throw new UnableToConnectToServerException(ex);

    }

}

// Somewhere upper the call stack

try

{

    await SendNotification(endpointUrl);

    MarkNotificationAsSent();

}

catch (UnableToConnectToServerException)

{

    MarkNotificationAsNotSent();

}

Such non-linear program flow can become a mess really quickly because it’s hard to trace all existing connections between throw and catch statements. No wonder such use of exceptions is often equated with goto statements.

Thankfully, the mess can be avoided relatively easily. You just need to explicitly return values indicating success or failure of an operation instead of throwing exceptions. This would bring clarity to potentially error-prone code parts (admittedly, at the expense of brevity). I recommend using some version of the Result class to do that.

However, not all programmatic failures should be treated that way, and in this post, I want to talk about the other side of the spectrum. That is when programmers employ Result in places where exceptions should be used.

But let’s step back for a second and recall types of failures from error handling perspective.

Different authors draw various sets of failure types but I personally think they all can be boiled down to the following two:

  • Errors we know how to deal with.
  • Errors we don’t know how to deal with.

The first one is exactly the type of errors the Result class is intended for. If you know how to process a failure, let alone expect that failure to happen, there’s no reason to use exceptions. It’s much better to be explicit about your intent and represent the result of the operation as a value so that you can handle it later the same way you handle other values:

public async Task<Result> SendNotification(string endpointUrl)

{

    HttpResponseMessage message;

    try

    {

        message = await _httpClient.PostAsync(endpointUrl);

    }

    catch (HttpRequestException ex)

    {

        return Result.Fail(“Unable to send notification to: “ + endpointUrl);

    }

    string response = await message.Content.ReadAsStringAsync();

           

    /* do something with the response */

 

    return Result.Ok();

}

// Somewhere upper the call stack

Result result = await SendNotification(endpointUrl);

if (result.IsSuccess)

{

    MarkNotificationAsSent();

}

else

{

    MarkNotificationAsNotSent();

}

Note that now there’s an explicit Result object that the client code can examine and use.

The other way to describe a failure you know how to deal with is using the phrase expected failure. You can’t work around a failure you didn’t expect, at least not in any meaningful way. Whenever your program can proceed with its execution flow after a failure has happened, the said failure becomes expected.

The code above knows what to do if the POST request fails, it’s an expected outcome. In this particular sample, the failure gets persisted to the database for later examination. And of course, there could be some sophisticated workaround in place of this logic, such as a re-try mechanism.

Let’s now look at another example. Let’s say we are registering a new customer in our system:

[HttpPost]

public HttpResponseMessage RegisterCustomer(string email)

{

    var customer = new Customer(email);

    Result saveResult = _repository.Save(customer);

 

    if (saveResult.IsFailure)

        return InternalServerError();

 

    return Ok();

}

// Repository

public Result Save(Customer customer)

{

    try

    {

        _session.SaveOrUpdate(customer);

        return Result.Ok();

    }

    catch (ADOException)

    {

        return Result.Fail(“Unable to save”);

    }

}

As you can see, this code sample also uses a Result instance. This instance shows whether or not saving the customer to the database succeeded. But is it the same kind of failure we saw in the previous example?

It is not.

Here, the RegisterCustomer method doesn’t continue executing after a failure takes place. This particular operation has a fundamental assumption: the ability to save a customer to the database. No further action can be taken unless this assumption is met. And indeed, the code doesn’t do anything other than returning an Internal Server Error (500) response (which is perfectly justified because this error represents something we don’t normally expect to happen).

So what we have here is precisely the second type of failure: a failure we don’t know how to deal with. The fact that our code knows about this possibility doesn’t make any difference in this situation as it won’t be able to mitigate it. Even if we wrap it into a Result instance, the best we can do is fail fast returning a 500 error to the client. It’s basically a dead-end, an exceptional situation for us.

And what programming feature helps us represent such exceptional situations? You got it, exceptions!

No need in the Result class as we are not able to make use of it anyway. Exceptions already have the semantics we look for: they can interrupt the current operation and bubble up to some generic exception handler at the very top of our execution stack where they can be transformed into a 500 response. Remember, never return 500s intentionally!

Here’s how the code can look like after refactoring:

[HttpPost]

public HttpResponseMessage RegisterCustomer(string email)

{

    var customer = new Customer(email);

    _repository.Save(customer);

 

    return Ok();

}

// Repository

public void Save(Customer customer)

{

    try

    {

        _session.SaveOrUpdate(customer);

    }

    catch (ADOException ex)

    {

        Log(ex);

        throw;

    }

}

Or, even better:

[HttpPost]

public HttpResponseMessage RegisterCustomer(string email)

{

    var customer = new Customer(email);

    _repository.Save(customer);

 

    return Ok();

}

// Repository

public void Save(Customer customer)

{

    _session.SaveOrUpdate(customer);

}

Any exception thrown by the data access code will be propagated and then caught and logged at the top-most level of the application’s execution stack in the generic exception handler – exactly the behavior suitable for such kind of failures.

Alright, so here’s the takeaway from this article: don’t catch exceptions you don’t know how to deal with. At all. The exception semantics is exactly what you need in this case: let the error bubble up and stop the current operation entirely. A corollary to this rule is that there’s no benefit in wrapping such exceptions using the Result class (or any other return value for that matter).

Summary

It’s pretty easy to differentiate use cases for Result and exceptions. Whenever the failure is something you expect and know how to deal with – catch it at the lowest level possible and convert into a Result instance. If you don’t know how to deal with it – let it propagate and interrupt the current business operation. Don’t catch exceptions you don’t know what to do about.





  • Marcel

    Interesting article. This advice is the direct opposite of what Uncle Bob argues in Clean Code (Chapter 7, page 104). I agree with your approach for application code (as your examples are). If you’re writing libraries of frameworks, you cannot know whether your client can deal with the failure. And therefore I think library code should throw exceptions. If applications can deal with the exceptions generated by the library code, they should wrap that code and return a Result. WDYT?

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      You described a correct behavior for libraries. And it doesn’t contract the advice from the article.

      As I mentioned in the post, if you don’t know how to deal with some failure/situation, throw an exception. If you do know that, return a Result instance. Note that it’s not the client of your library whose ability to deal with that failure you need to know about, it’s *your*, library author’s ability. In other words, if you as a library author don’t know what to do with some failure, wrap it with your own exception and let it propagate further. For the client, the same failure might be an expected one so they can decide to convert it into a Result instance on their end.

      Let’s say for example that you write an ORM. Inability to connect to the database is a failure that you, as the library author, don’t know how to deal with. The solution here is highly contextual and depends on the client application specific, you can’t possibly know that specific. So the only way for you here is to throw an exception. On the other hand, the client of the ORM might expect their database to go offline from time to time, so for them, the failure is expected. In this case, they wrap your exception with a Result instance and deal with it as with a regular value from there.

  • Peter

    As far as I know in Java (CleanCode) you have to specify the exceptions in your method, so the caller knows that the method can throw exceptions and he/she can prepare for that (like a contract).
    However, in C# it is harder to see what method can throw what exception, and if the call stack is really deep it is impossible to know/handle all exceptions.

    • http://etienne.mermillod.net Mermich

      C# is different in Java for that, you can’t know what kind of exeption a method could throw.

      • Jaime Lajarin

        You can in fact know if the code was properly documented. If the method includes the metadata, this will show in the intellisense.

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      In Java, the situation with exceptions is indeed better, but not much. While you do have them specified in the method signature, the mechanics of throwing and catching is still similar to goto. For example, you might have a try/catch block with several statements in it and there’s no way to figure out which of them actually throws. To do that, you would need to look at signatures of each of these methods.

      So, something like:
      try
      {
      Method1();
      Method2(); // Only this method throws
      Method3();
      }
      catch (Exception)
      {}

  • JeanFulbert

    Very interesting article!

    I have a short question, what about guard clauses in the domain layer?

    I use exceptions if a parameter of my constructor is null. But it goes against the “don’t catch exceptions you don’t know how to deal with” principle you describe in this article. If I follow your rule, I’ll have a private constructor and static methods which returns a Result.

    How do you deal with this problem?

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      It depends on whether you catch those exceptions or not. If you just throw them, to show a contract violation, that’s perfectly fine (in fact, that’s the desired behavior, see this post: http://enterprisecraftsmanship.com/2015/02/14/code-contracts-vs-input-validation/ ). If you are catching those exceptions, then you really need a Result instance, so your the solution you proposed with a private constructor and a static method would be a better choice.

  • Sergio Rykov

    Agreed.

    I came up to the same conclusions working on a project where we used Option and Result very extensively.

    You can catch DB or any external services exceptions if you know how to process it. It works pretty well with retry policies (Polly/EntLib) to handle transient errors.

  • Kirk Larkin

    Nice article. A much more detailed explanation than the usual “use exceptions for exceptional situations”, which is vague and subjective.

    I’m working on a C# project where, for example, I need to update an entity in the database. The controller takes in a view-model and passes it through Jimmy Bogard’s MediatR into a handler. The first thing the handler does is check that the entity exists in the database – if it doesn’t, we can’t continue and need to get a 404 back out of the controller at the request/response level. What do you think is the best way to propagate this back up to the controller? From reading this article, I think the exception approach holds, as the handler can’t continue in this scenario, but it’s not clear to me.

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      That would actually be a validation error. The best way to handle those is with return values (Result instances). Validation errors are not exceptional as they don’t constitute a bug or a hardware failure you don’t know what to do about.

      Overall, there are different ways to deal with validation (here I wrote more on this: http://enterprisecraftsmanship.com/2016/09/13/validation-and-ddd/ ) and they all have their own pros and cons. But in general, I would recommend you take the approach with Result. It’s less declarative (comparing to something like FluentValidations) but more explicit in terms of usage and thus easier to follow.

      • Kirk Larkin

        Thanks for the explanation and link.