Functional C#: Handling failures, input errors

The topic described in this article is a part of my Applying Functional Principles in C# Pluralsight course.

In this article, I’m going to write about how to deal with failures and invalid input in a functional way.

Handling errors in C#: the common approach

The concept of validation and error processing is well known, but the code required to handle it may become really annoying in languages like C#. This article is inspired by Railway Oriented Programming which was introduced by Scott Wlaschin in his talk at NDC Oslo. I encourage you to watch the full video as it gives invaluable insights of how awkward our day-to-day C# code might be.

Look at the sample below:

[HttpPost]
public HttpResponseMessage CreateCustomer(string name, string billingInfo)
{
    Customer customer = new Customer(name);
 
    _repository.Save(customer);
 
    _paymentGateway.ChargeCommission(billingInfo);
 
    _emailSender.SendGreetings(name);
 
    return new HttpResponseMessage(HttpStatusCode.OK);
}

It seems easy and straightforward: first we create a customer instance, then save it, after that charge some commission and finally send a greetings e-mail. The problem with this code is that it handles the happy path only, i.e. the path in which everything goes just fine.

When you start taking into account potential failures, input errors and logging routine, the method starts turning into boilerplate code:

[HttpPost]
public HttpResponseMessage CreateCustomer(string name, string billingInfo)
{
    Result<CustomerName> customerNameResult = CustomerName.Create(name);
    if (customerNameResult.Failure)
    {
        _logger.Log(customerNameResult.Error);
        return Error(customerNameResult.Error);
    }
 
    Result<BillingInfo> billingInfoResult = BillingInfo.Create(billingInfo);
    if (billingInfoResult.Failure)
    {
        _logger.Log(billingInfoResult.Error);
        return Error(billingInfoResult.Error);
    }
 
    Customer customer = new Customer(customerNameResult.Value);
 
    try
    {
        _repository.Save(customer);
    }
    catch (SqlException)
    {
        _logger.Log("Unable to connect to database");
        return Error("Unable to connect to database");
    }
 
    _paymentGateway.ChargeCommission(billingInfoResult.Value);
 
    _emailSender.SendGreetings(customerNameResult.Value);
 
    return new HttpResponseMessage(HttpStatusCode.OK);
}

Even worse, if we need to handle failures in both Save and ChargeCommission methods, we end up creating compensation logic so that the changes could be rolled back if one of the methods fails:

[HttpPost]
public HttpResponseMessage CreateCustomer(string name, string billingInfo)
{
    Result<CustomerName> customerNameResult = CustomerName.Create(name);
    if (customerNameResult.Failure)
    {
        _logger.Log(customerNameResult.Error);
        return Error(customerNameResult.Error);
    }
 
    Result<BillingInfo> billingIntoResult = BillingInfo.Create(billingInfo);
    if (billingIntoResult.Failure)
    {
        _logger.Log(billingIntoResult.Error);
        return Error(billingIntoResult.Error);
    }
 
    try
    {
        _paymentGateway.ChargeCommission(billingIntoResult.Value);
    }
    catch (FailureException)
    {
        _logger.Log("Unable to connect to payment gateway");
        return Error("Unable to connect to payment gateway");
    }
 
    Customer customer = new Customer(customerNameResult.Value);
    try
    {
        _repository.Save(customer);
    }
    catch (SqlException)
    {
        _paymentGateway.RollbackLastTransaction();
        _logger.Log("Unable to connect to database");
        return Error("Unable to connect to database");
    }
 
    _emailSender.SendGreetings(customerNameResult.Value);
 
    return new HttpResponseMessage(HttpStatusCode.OK);
}

You can see that our 5 lines have turned into 35 - the method has become 7 times longer! It is now really hard to follow the program flow. Those 5 lines of meaningful code are now buried under the bulk of boilerplate orchestration.

Handling failures and input errors in a functional way

Can it be fixed? Luckily, yes. Let’s go through the method and see what we can do with it.

You might have noticed that we use the technique I described in my primitive obsession article: instead of using the raw name and billingInfo strings, we are wrapping them with CustomerName and BillingInfo classes. That gives us an opportunity to put all the relevant validation logic in one place and comply with the DRY principle.

The static Create method returns a special class named Result which encapsulates all information regarding the operation’s results: an error message in case it failed and an object instance in case it succeeded.

Also, note that potential failures are wrapped with try/catch statement. Such approach breaks one of the best practices I wrote about in my Exceptions for flow control post. It states that if you know how to deal with exceptions, catch them at the lowest level possible.

It means that ChargeCommission and Save methods should catch known exceptions by themselves and return a result just as the static Create methods do. Let’s refactor the code:

[HttpPost]
public HttpResponseMessage CreateCustomer(string name, string billingInfo)
{
    Result<CustomerName> customerNameResult = CustomerName.Create(name);
    if (customerNameResult.Failure)
    {
        _logger.Log(customerNameResult.Error);
        return Error(customerNameResult.Error);
    }
 
    Result<BillingInfo> billingIntoResult = BillingInfo.Create(billingInfo);
    if (billingIntoResult.Failure)
    {
        _logger.Log(billingIntoResult.Error);
        return Error(billingIntoResult.Error);
    }
 
    Result chargeResult = _paymentGateway.ChargeCommission(billingIntoResult.Value);
    if (chargeResult.Failure)
    {
        _logger.Log(chargeResult.Error);
        return Error(chargeResult.Error);
    }
 
    Customer customer = new Customer(customerNameResult.Value);
    Result saveResult = _repository.Save(customer);
    if (saveResult.Failure)
    {
        _paymentGateway.RollbackLastTransaction();
        _logger.Log(saveResult.Error);
        return Error(saveResult.Error);
    }
 
    _emailSender.SendGreetings(customerNameResult.Value);
 
    return new HttpResponseMessage(HttpStatusCode.OK);
}

As you can see, now both ChargeCommission and Save methods return Result objects.

The goal of the Result class is pretty simple and very similar to the Maybe monad we discussed earlier: it allows us to reason about the code without looking into the implementation details. Here’s how it looks like (I omitted some details for brevity):

public class Result
{
    public bool Success { get; private set; }
    public string Error { get; private set; }
    public bool Failure { /* ... */ }
 
    protected Result(bool success, string error) { /* ... */ }
 
    public static ResultFail(string message) { /* ... */ }
 
    public static Result<T> Ok<T>(T value) {  /* ... */ }
}
 
public class Result<T> : Result
{
    public T Value { get; set; }
 
    protected internal Result(T value, bool success, string error)
        : base(success, error)
    {
        /* ... */
    }
}

Now, we can apply the same principle that functional languages use. That is where the actual magic happens:

[HttpPost]
public HttpResponseMessage CreateCustomer(string name, string billingInfo)
{
    Result<BillingInfo> billingInfoResult = BillingInfo.Create(billingInfo);
    Result<CustomerName> customerNameResult = CustomerName.Create(name);
 
    return Result.Combine(billingInfoResult, customerNameResult)
        .OnSuccess(() => _paymentGateway.ChargeCommission(billingInfoResult.Value))
        .OnSuccess(() => new Customer(customerNameResult.Value))
        .OnSuccess(
            customer => _repository.Save(customer)
                .OnFailure(() => _paymentGateway.RollbackLastTransaction())
        )
        .OnSuccess(() => _emailSender.SendGreetings(customerNameResult.Value))
        .OnBoth(result => Log(result))
        .OnBoth(result => CreateResponseMessage(result));
}
 

If you are familiar with functional languages, you might have noticed that OnSuccess extension method is actually a Bind method. I named it that way just to make it clear how exactly it works.

What the OnSuccess method basically does is it checks the previous Result instance and if it is successful, executes the delegate passed in; otherwise, it just returns the previous result. Thus, the chain continues right until one of the operations fails. And if it does, the other operations are getting skipped.

OnFailure method, as you might guessed, is executed only if the previous operation failed. It is a perfect fit for the compensation logic we need to perform in case the database call wasn’t successful.

OnBoth is placed at the end of the chain. It’s main use cases are logging the operations' failure and creating the resulting messages.

So what we got here is the exact same behavior we had before but with much less of boilerplate code. As you can see, the program flow has become much easier to follow.

What about the CQS principle?

What about the Command-Query Separation principle? The approach described above implies using return value (which is, in our case, an instance of the Result class) even if the method itself is a command (i.e. changes the object’s state). Is there a conflict with CQS?

No, there isn’t. Moreover, this approach increases readability in the same way following the CQS principle does. Not only does it allow you to know if a method is a command or a query, but it also allows you to see whether or not the method may fail.

Designing for failures extends the potential range of information you get from the method’s signature. Instead of 2 results (void for commands and some type for queries) you now have 4 of them.

  • The method is a command and it can’t fail:

public void Save(Customer customer)
  • The method is a query and it can’t fail:

public Customer GetById(long id)
  • The method is a command and it can fail:

public Result Save(Customer customer)
  • The method is a query and it can fail

public Result<Customer> GetById(long id)

And when I write that a method can’t fail I don’t mean it can’t fail in any given circumstances. Of course, there always is a chance of getting some kind of exception that wasn’t expected in the first place. What I mean is that the method is designed to always succeed, i.e. the developer supposes that any exception thrown within that method is unexpected (see also Exceptions for flow control in C# article for more details about expected and unexpected exceptions).

With this approach, exceptions become the thing they were intended to be initially: they signalize that there’s something wrong with your system. From that moment, they become a really helpful assistant on the way of building software.

Summary

Exposing your intent is crucial if you want to increase readability of your code. Introducing the Result class helps to show if the method can fail or not. OnSuccess, OnFailure, and OnBoth methods, on the other hand, help you remove boilerplate code resulting in a clean and narrow design.

In conjunction with other 3 techniques - immutability, getting rid of primitive obsession and non-nullable reference types - this approach introduces a powerful programming model that can significantly increase your productivity.

Update

I’ve created a NuGet package out of the Result class and the extensions that work on top of it: link. If you want to request an addition of overloads to support your use cases, leave a comment here.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus