In this article I am going to look at a number of different approaches to model the results of a complex operation in C#. This is a technique I find useful when I have to perform some logic, and it can return different types of results, depending on the outcome. I’ll start with some naive approaches, before looking at two options that look more promising.

I’ll be using the following logic for my “complex operation”:

if length(s) is even
    return length(s);
else
    return "Length is odd";

With an additional a requirement that any exceptions in the “complex operation” will be handled in a suitable way.

This is actually something I have had to implement at work in a number of cases; try an operation, then depending on the outcome handle it in the most appropriate way.

In the examples I’ll be using Console.WriteLine for simplicity, but in the real world there could be database calls, UI updates, HTML rendering, service calls, whatever usually makes testing hard.

The inputs to and outputs from every example will be the same.

Inputs:

  • "Food" (returns 4)
  • "Foo" (returns Length is odd.)
  • null (returns NullReferenceException)

All the code for the following is available in my GitHub repo ResultType-blog - these are Linqpad scripts, but can easily be modified to be C# by removing the first line.

1. Just do it

Here’s the most trivial approach to solve the issue:

void Main()
{
    ComplexOperation("Food");
    ComplexOperation("Foo");
    ComplexOperation(null);
}

void ComplexOperation(String input)
{
    try
    {
        if (input.Length % 2 == 0)
            Console.WriteLine($"Even length: {input.Length}.");
        else
            Console.WriteLine("Length is odd.");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
}

This meets our requirements, but lets see if we can see a few issues with it.

Firstly, it isn’t possible to test the Complex Operation on it’s own, you have no way to mock out the dependencies. Secondly, you’re mixing business logic, with side effects. Readers of Mark Seemann’s blog will know that this makes code harder to reason about.

A common approach to solve this is to introduce a type to model the result of the complex operation. The remaining examples look at different approaches to do this.

2. Implicit Results

Let’s start with an Implicit Result type:

class Result
{
    public String FailureMessage { get; set;}
    public Int32? EvenLength { get; set; }
    public Exception Error { get; set;}
}

I call it Implicit because if I passed you an instance of it, you have no obvious way of knowing what the result is or how to figure out what happened during the complex operation. You could check that EvenLength is not null and assume success, but what’s to say I didn’t put set it to 0 and populate FailureMessage instead? It it mostly guess work and assumptions to know what this result contains.

Here’s the new program and complex operation using this type:

void Main()
{
    var inputs = new[] { "Food", "Foo", null, };

    foreach (var result in inputs.Select(ComplexOperation))
    {
        if (result.Error != null)
            Console.WriteLine(result.Error);
        else if (result.FailureMessage != null)
            Console.WriteLine(result.FailureMessage);
        else
            Console.WriteLine($"Even length: {result.EvenLength}.");
    }
}

Result ComplexOperation(String input)
{
    try
    {
        if (input.Length % 2 == 0)
            return new Result { EvenLength = input.Length, };
        else
            return new Result { FailureMessage = "Length is odd.", };
    }
    catch (Exception ex)
    {
        return new Result { Error = ex, };
    }
}

Now you see the implementation you know there is no funny business, but I made you read the complex operation to be sure.

There are some other problems with this too:

  • The type is mutable, meaning something might change the result after the operation.
  • When the type is constructed, it is half-full, some members will have values, some won’t.
  • You have to know how to process a result, I’d find myself asking can an error also have a message?.
  • It violates the Open/closed principle because changes to Success, Failure or Error require a change to this one type.

This does, however, separate the operation from the outputs, making the operation testable without it needing any additional depencies for the output. The operation is now Pure, which is why I can use Select on each input to return the result.

3. Explicit Results

I’ve harped on enough about how bad it it that the result is implied in the previous example. So let’s have a go at been more explicit.

Here’s a result type with an enum to tell you what happened:

class Result
{
    public String FailureMessage { get; set;}
    public Int32? EvenLength { get; set; }
    public Exception Error { get; set; }
    public ResultType Type { get; set; }
}

enum ResultType
{
    Success,
    Failure,
    Error,
}

Now when you get a result you can first check the Type and know it is an error.

Here’s the application code for it:

void Main()
{
    var inputs = new[] { "Food", "Foo", null, };

    foreach (var result in inputs.Select(ComplexOperation))
    {
        switch (result.Type)
        {
            case ResultType.Success:
                Console.WriteLine($"Even length: {result.EvenLength}.");
                break;
            case ResultType.Failure:
                Console.WriteLine(result.FailureMessage);
                break;
            case ResultType.Error:
                Console.WriteLine(result.Error);
                break;
        }
    }
}

Result ComplexOperation(String input)
{
    try
    {
        if (input.Length % 2 == 0)
            return new Result { EvenLength = input.Length, Type = ResultType.Success, };
        else
            return new Result { FailureMessage = "Length is odd.", Type = ResultType.Failure, };
    }
    catch (Exception ex)
    {
        return new Result { Error = ex, Type = ResultType.Error, };
    }
}

This still has some of the problems of the previous version: mutability, Open/closed principle, mixed bag of properties, and not knowing what to do with them. It also has the problem that nothing is forcing you to check the Type, I might just say It’s OK, this won’t fail, just get my the EvenLength - famous last words…

So, whilst it is a little better, it can still lead to unreasonable code.

4. Explicit - with factory methods

To solve the problem of people creating a “mixed bag” of mutable properties, a factory method could be created on the type to initialise the result in the correct state depending on the outcome of the operation.

class Result
{
    public String FailureMessage { get; private set; }
    public Int32? EvenLength { get; private set; }
    public Exception Error { get; private set; }
    public ResultType Type { get; private set; }

    public static Result CreateFailure(String message)
    {
        return new Result { FailureMessage = message, Type = ResultType.Failure, };
    }

    public static Result CreateSuccess(Int32 value)
    {
        return new Result { EvenLength = value, Type = ResultType.Success, };
    }

    public static Result CreateError(Exception ex)
    {
        return new Result { Error = ex, Type = ResultType.Error, };
    }
}

This changes the complex operation to look like this:

Result ComplexOperation(String input)
{
    try
    {
        if (input.Length % 2 == 0)
            return Result.CreateSuccess(input.Length);
        else
            return Result.CreateFailure("Length is odd.");
    }
    catch (Exception ex)
    {
        return Result.CreateError(ex);
    }
}

The rest of the program is unchanged from the previous version.

We now have a way to know that the Result with the type success will only have an EvenValue, however we still need to ignore the other properties that don’t relate to success. There’s still nothing forcing people to check the Type, and this requires additional factory methods for every state.

I’ve seen a number of people stop at this level, and call it “good enough” to avoid having to go to the next level. You still have unreasonable code, and have to understand things in the operation.

5. Exceptions for control flow

This is another approach I have seen used, I do not like it, but I thought I would include it, as I almost used it years ago before using one of the approaches in the following sections.

void Main()
{
    var inputs = new[] { "Food", "Foo", null, };

    foreach (var input in inputs)
    {
        try
        {
            var result = ComplexOperation(input);
            Console.WriteLine($"Even length: {result}.");
        }
        catch (BusinessException be)
        {
            switch (be)
            {
                case FailureException f:
                    Console.WriteLine(f.Message);
                    break;
                case ErrorException e:
                    Console.WriteLine(e.InnerException);
                    break;
                default:
                    throw;
            }
        }
    }
}

Int32 ComplexOperation(String input)
{
    try
    {
        if (input.Length % 2 == 0)
            return input.Length;
        else
            throw new FailureException("Length is odd.");
    }
    catch (Exception ex) when (!(ex is BusinessException))
    {
        throw new ErrorException(ex);
    }
}

class FailureException : BusinessException
{
    public FailureException(String message) : base(message) { }
}

class ErrorException : BusinessException
{
    public ErrorException(Exception inner) : base(inner) { }
}

abstract class BusinessException : Exception
{
    public BusinessException(String message) : base(message) { }
    public BusinessException(Exception inner) : base("Something bad happened", inner) { }
}

I hope by looking at this code you can see it isn’t an ideal appraoch.

I’ve introduced the concept of a BusinessException that the program will handle in a try...catch block. All problems in the complex operation will throw some sort of exception derived from BusinessException, which the program will then type match on. I’ve used pattern matching here, but I’ve see other approaches such as a Dictionary<Exception, Action<Exception>> that has a list of exceptions and the delegate to call.

Using exceptions like this is the equivelent of goto, many people have said it before, so I won’t go into detail on that aspect. I did notice when writing this how hard it is to not accidentally catch your own BusinessException, this is why I have an exception filter to not handle them twice: catch (Exception ex) when (!(ex is BusinessException)). I could imagine the case where one stray try...catch could cause a lot of problems.

6. Type per Result

I’ve now harped one enough about not knowing what to do with results. This example removes the ambiguity and uses a separate type for each result.

class Success : Result
{
    public Int32 EvenLength { get; }
    public Success(Int32 value) { EvenLength = value; }
}
class Failure : Result
{
    public String FailureMessage { get; }
    public Failure(String message) { FailureMessage = message; }
}

class Error : Result
{
    public Exception Exception { get; }
    public Error(Exception ex) { Exception = ex; }
}

abstract class Result
{
}

Each result now has its own type. Each type only has properties relating to that type of result. The results are immutable.

The base class in this case is empty, but it might capture the input, elapsed time, or anything else you need in every result.

The program and complex operation now are much easier to reason about, and it is a lot harder to mix things up:

void Main()
{
    var inputs = new[] { "Food", "Foo", null, };

    foreach (var result in inputs.Select(ComplexOperation))
    {
        switch (result)
        {
            case Success s:
                Console.WriteLine($"Even length: {s.EvenLength}.");
                break;
            case Failure f:
                Console.WriteLine(f.FailureMessage);
                break;
            case Error e:
                Console.WriteLine(e.Exception);
                break;
        }
    }
}

Result ComplexOperation(String input)
{
    try
    {
        if (input.Length % 2 == 0)
            return new Success(input.Length);
        else
            return new Failure("Length is odd.");
    }
    catch (Exception ex)
    {
        return new Error(ex);
    }
}

I’m using C# 7’s Pattern Matching feature in the program to compare each type of the result. When I get a match it is already cast into the correct type, so s will be an instance of Success, and Success only has properties relating to a successful outcome.

To me this is very clear what I can do next after a complex operation and what happened in the operation.

It is reassuring to know that if I have a Success type I can only see properties relating to a successful operation, I can pass the result to another method, that accepts an instance of Success knowing it can’t be called with an Error by mistake - the type safety in the language is on your side.

Consider these 2 methods:

void DisplaySuccess(Result r) { }
void DisplaySuccess(Success s) { }

In all previous examples you had to have the first version, and that would either assume you called it correctly, or would have to check that r is a success. The second method can only be called with an instance of Success, you cannot pass an Error to it, making it much harder to get wrong.

There are a few negatives to this approach, in C#’s pattern matching the compiler doesn’t check you have every case matched, adding a new result type means I need to find all instances of result handlers and update them. If you have only one handler, then this isn’t so bad.

Another consideration is that the result’s “next step” logic - what happens next - is separated from the type. Sometimes this could be desirable, other times you might want it contained in a single place, it depends on how your application is designed and what works best. The next exmaple looks at keeping the behaviour with the result.

7. Types with Behaviour

I’ve fleshed the following code out a little more, to highlight one of the drawbacks of the approach. In all previous examples, I’ve left out how you might test the entire operation - passing in test doubles for Console.WriteLine into the program from the composition root would be trivial.

However, in this case I wanted to show the extra effort needed to keep things testable.

First, we’ll look at the base result type:

abstract class Result
{
    public Result(IProcessor processor)
    {
        Processor = processor;
    }

    protected IProcessor Processor { get;}

    public abstract void Process();
}

interface IProcessor
{
    void WriteMessage(Object message);
}

class Processor : IProcessor
{
    public void WriteMessage(Object message) => Console.WriteLine(message);
}

There’s now an additional Process member on every result and every result needs access to a IProcessor which facilitates the injection of the dependencies for the Process method.

This is what the calling program will use to handle the result:

void Main()
{
    var inputs = new[] { "Food", "Foo", null, };

    foreach (var result in inputs.Select(ComplexOperation))
    {
        result.Process();
    }
}

This looks very neat, I get a result, I call Process.

The problems are getting the dependencies managed in an nice way. When deriving instances of the Result you need to write the code to pass IProcessor through:

class Success : Result
{
    public Int32 EvenLength { get; }
    public Success(IProcessor p, Int32 value) : base(p) { EvenLength = value; }
    public override void Process() => Processor.WriteMessage($"Even length: {EvenLength}.");
}

class Failure : Result
{
    public String FailureMessage { get; }
    public Failure(IProcessor p, String message) : base(p) { FailureMessage = message; }
    public override void Process() => Processor.WriteMessage(FailureMessage);
}

class Error : Result
{
    public Exception Exception { get; }
    public Error(IProcessor p, Exception ex) : base(p) { Exception = ex; }
    public override void Process() => Processor.WriteMessage(Exception);
}

Each result now has an implementation of the logic to handle it. If you want to know what happens given a success, I can just look at the Success type.

But when you create an instance, you also need to pass in an IProcessor, so the complex operation will have to do this:

IProcessor processor = new Processor();

Result ComplexOperation(String input)
{
    try
    {
        if (input.Length % 2 == 0)
            return new Success(processor, input.Length);
        else
            return new Failure(processor, "Length is odd.");
    }
    catch (Exception ex)
    {
        return new Error(processor, ex);
    }
}

This is quite a lot of ceremony, and now the complex operation has knowledge of the IProcessor. An instance of an IProcessor would have to be injected so that it can be passed into each result. The complex operation doesn’t depend on IProcessor though, just the results, making this a kind of transient dependency.

This example isn’t perfect, but I have used it in a number of places where I wanted to keep the logic of what to do with a result with the result, and not separated out across the code base. Usually when there’s a lot of code related to handling the result.

I also like that I am able to write code such as:

var result = ComplexOperation(input);
resut.Process();

If you need to add a new result, type (e.g. Timeout) you can do so by just deriving a new type from Result and implementing all the logic there. The only other place that needs a change is the complex operation to return new Timeout(processor), the program doesn’t have to change.

8. Bonus F# version

I am a big fan of F# so I thought I would model the same problem in F#.

I’ve deliberately kept it similar to the C# examples to avoid it getting too functional. This is quite close to example #6 above.

type Result =
    | Success of int
    | Failure of string
    | Error of Exception

(* Unchecked.defaultof<String> is used for null to make it crash - F# doesn't do null really. *)
let inputs = [ "Food"; "Foo"; Unchecked.defaultof<String>; ]

let complexOp (input: string) =
    try
        if input.Length % 2 = 0 then
            Success input.Length
        else
            Failure "Length is odd."
    with
    | ex -> Error ex

let processResult r =
    match r with
    | Success s -> printfn "Even length: %d" s
    | Failure f -> printfn "%s" f
    | Error e -> printfn "%A" e

let main () =
    let results =
        inputs
        |> Seq.map complexOp

    results |> Seq.iter processResult

main ()

I’ve modelled the result as a Discriminated Union with 3 cases, one for each outcome. The complex operation, like the C# version returns 1 of these 3 cases. What is nice in F# is that in processResult where I take in a single result and handle it, the pattern match must be complete. If I added another case to the Result type, the compiler will complain that is isn’t handled in the match.

Conclusion

This won’t be an exhaustive list of ways to handle results, but it does provide some different approaches to the problem that should help keep your code base a little cleaner. Options 6 and 7 are ones I would use in C#, the rest create unreasonable code that I would not like to have to think about. The complex operation is never going to be a few lines of code like in my scenario, it might be many classes working to do many different operations, building one final result. I like it when I don’t have to know the implementation details of an operation to know what the behaviour is for a given outcome.

Above, I have only used Success, Failure and Error as the outcomes of my operation, but I could have modelled different states for success too: a MatchFound/NoMatch result could be suitable for a result type.