Handling results in code can be become tedious, repetitive, and difficult to follow. The traditional method works but can evolve into code that is hard to maintain. There is another way that is not overly complex and promote better practices.
The Traditional Way
It all starts very simply. You have a method that you call from your user interface that does some work. All you want to do is fire and forget.
DoSomething(withSomething);
void DoSomething(withSomething)
{
Console.WriteLine("Start the Process");
Console.WriteLine("The Process finished");
}
However, you quickly discover problems. The method throws an exception perhaps because you passed a Null value or some other value that the method does not like. So, you wrap the code into a try/catch block, and you catch Exception.
try
{
DoSomething(withSomething);
}
catch(Exception)
{
Console.WriteLine("The Process failed");
}
Now you catch every random Exception including those that may be coming from other code called by the method. But telling your non-programmer user that the process failed isn’t very helpful. So, you output the Exception. Your non-programmer user doesn’t understand exceptions, but at least they can send it to you when they complain that your program doesn’t work. You might then be able to add more specific exceptions to your try/catch block and input validation to address the issues that come up. But now your UI code is filled with try/catches that require deep knowledge of the method called and all the possible outcomes.
With the exceptions out of the way, now you run into a different problem. Now your users complain that the value returned is sometimes incorrect. It takes you awhile, but eventually you discover the value you received from the method was wrong and that wrong value has now been propagated to other parts of your system. So, now you start checking the value result for conditions like null, empty, or zero.
You have code that looks like this:
try
{
Console.WriteLine("Start the Process");
if(withSomething == 0)
Console.WriteLine("The value you provide must be greater than 0");
if(withSomething > 100)
Console.WriteLine("The value you provide must be less than 100");
DoSomething(withSomething);
Console.WriteLine("The Process finished");
}
catch(ArgumentNullException)
{
Console.WriteLine("The value you provide must have a value");
}
catch(Exception ex)
{
Console.WriteLine("The Process failed due to: {0}", ex);
}
All you really want is a value or an error. Instead, you must write all this code with knowledge of everything that can go wrong.
There is an alternative.
Introducing Results
There are many patterns and practices you can introduce to simplify the code and address the problems. One I particularly like is using a Result type.
Very simply, a Result type can replace the void return type in the DoSomething method from the example above. At the base, it simply tracks if a procedure was successful or not. It should be immutable because the results of an operation shouldn’t change after the operation is finished. You can rerun the operation, but that would be a new result.
public record struct Result
{
public bool IsSuccessful { get; init; }
}
You could make the Result a regular class. However, I prefer to implement it as a record struct. If you think about it, the result of an operation should be a short lived value that does not change. This is a textbook use of struct. You lose the inheritance available in a class, but you really shouldn’t need to create a bunch of inherited Result types. The benefit is the short lived Result value will be on the Stack instead of the Heap and thereby avoid the overhead of garbage collection. Get a result, use it, and move on.
✔️ CONSIDER defining a struct instead of a class if instances of the type are small and commonly short-lived or are commonly embedded in other objects.
Choosing Between Class and Struct – Framework Design Guidelines | Microsoft Learn
Adding Errors
The result we just designed is not terribly useful as is. It’s really just an indirection of the bool. The only feature you get is the immutability of the IsSuccessful property. At this stage you would be better off changing your DoSomething method to a bool return type, but we are not done yet. What we are missing is a way to collect what went wrong. The simple way to start is to collect any Exceptions.
private readonly List<Exception> mErrors;
protected ReadOnlyCollection<Exception> Errors => !Successful
? mErrors
: throw new Exception("Successful Results should not have Errors.");
You might notice the Errors are protected and therefore not visible outside the record. At first glance, this will not appear to be terribly useful. The point is to protect against invalid state. Some people expose both the Errors and Value in the Result. However, this can become a problem particularly if both are read/write. You might end up in a bad state where you have Errors and also a Value. Was the operation successful? It will become a little clearer in the next step and later when we deal with values in the Result.
Building Results
Before I get to that, we need to create a constructor for the class. Here I like to block the creation of the Result type to better control what goes into the Result and avoid ending in a bad state. You could do it with multiple constructors, but then you end up with a bunch of checks that can get confusing overtime. By protecting the constructor, you can then limit object creation to certain conditions set in static methods.
internal Result(bool successful, List<Exception> errors)
{
if (successful && !errors.IsEmpty)
throw new Exception("A successful result should not have errors.");
if (!successful && errors.IsEmpty)
throw new Exception("A failed result should have errors.");
Successful = successful;
mErrors = errors;
}
First we check that a successful Result does not have errors and that a failed result does have errors. You could if you want treat errors as warnings and include them in a successful result, but my preference is to make the result exclusive. To create a Result, you still need to add static factory methods.
public static Result Success() => new(true, new());
public static Result Failure(Exception error) => new(false, new() {error});
Why all of this fuss? Again, we want to protect against bad state. The constructor can initialize all the variables. The static factory methods help you control what the caller supplies for input to avoid bad states.
Matching Success or Failure
Now that we have most of the plumbing out of the way, we can get back to adding value to the Result. Since we made the Errors unreachable, we need to provide a way for the consumer to get the Errors. But, we only want to have and give Errors if the Result was not successful. We do this with a bit of generics and Func magic.
public R Match<R>(
Func<R> success,
Func<ReadOnlyCollection<Exception>, R> failure) =>
IsSuccessful ? success() : failure(Errors);
If you are not familiar with Func and generics this might look a little odd. Very simple, we are creating a function that has a generic return type to selectively return a different value based on the Success of the Result. The value returned is based on the two function delegates provided by the caller. This means you can provide one function that returns a value when successful and another function that can use the Error list to return a different value. Still lost? Here it is in action.
string ResultMessage = Result.Match(
() => "The operation succeeded",
(errors) => $"The operation failed with {errors.Count});
Implementing Result
Of course, to evaluate the Result, we must get the result from some place. We can do that by refactoring the DoSomething method from the first example.
Result DoSomething(string withSomething)
{
Console.WriteLine("Start the Process");
if(withSomething is not null)
{
Console.WriteLine("The Process finished");
return Result.Successful();
}
return Result.Failure(new ArgumentNullException());
}
The new method checks that withSomething is not null and returns a Success Result if it is not. If withSomething is null, then we return a Failure Result with an ArgumentNullException. You could also wrap methods in a try/catch block to do the same.
Automating Conversions
We can do one more thing to make the Result even easier to use. Rather than using the factory methods to create results, lets create implicit conversions to do the creation for us.
public static implicit operator Result(bool success) => return new(success, new());
public static implicit operator Result(Exception error) => return new(false, new() { error });
Now we can further refactor the DoSomething method. This time we will substitute a bool value or exception for the Result type. The method still returns a Result. However, when you return true, it will be implicitly converted to a Success Result. When you pass an Exception, it will be implicitly converted to a Failure Result. The catch is you could return false and end up with a Failure Result that has no errors. That should be a bad state and strictly speaking, it might be better to leave out the implicit bool operator. However, I find it useful and it could be a stand in for unknown failures.
Result DoSomething(string withSomething)
{
Console.WriteLine("Start the Process");
if(withSomething is not null)
{
Console.WriteLine("The Process finished");
return true;
}
return new ArgumentNullException();
}
Handling Values
Now we have a working Result type. But it could be better. There are methods out there that return something other than void. How about adding a value to the Result?
Struct or Class
To add values, you’ll need a new record struct with a generic Value type. From a coding perspective, this is where classes would be more convenient. If Result were a class, then you would simply create a new Result<T> and inherit from Result. However, we made a record struct which means it is a sealed type that cannot inherit. Here I wouldn’t mind if you part ways with me and go for a class instead. I’ll stick with a struct and copy past Result into a new file Result<T>.
Adding Value
Let’s start by adding a Value. We will create the Value the same way with did the Errors, as a protected scope.
private readonly T? mValue;
protected T? Value => Successful
? mValue!
: throw new Exception("Failure Results should not have a value.");
Building Results
We will of course need to change our constructor to allow for a Value.
protected internal Result(bool successful, List<Exception> errors, T? value = default)
{
IsSuccessful = successful;
mErrors = errors;
mValue = value;
}
Next, we need to add our factory methods to account for the new Value. The unique thing in my approach is that the factory methods can go in the Result class instead of the generic Result<T> class. Now, why would you do that? This simplifies your code. It allows you to call Result.Success() and get a Result<T> instead of needing to remember to use Result and Result<T> separately.
public static Result<T> Success<T>(T value) => new(true, new(), value);
public static Result<T> Failure<T>(Exception error) => new(false, new(error));
public static Result<T> Failure<T>(List<Exception> errors) => new(false, new(errors));
Automating Conversions
Don’t forget our implict operators to make life easier. Go back to Result<T> and add the following:
public static implicit operator Result<T>(T value) => return new(true, new(), value);
public static implicit operator Result<T>(Exception exception) => return new(false, new(new List() { exception }));
public static implicit operator Result<T>(List<Exception> errors) => return new(false, errors);
Matching Success or Failure
Finally, we need our Match method. This time the Successful condition will return the string value. The failure condition remains the same.
public R Match<R>(
Func<T, R> success,
Func<List<Exception>, R> failure) =>
Successful ? success(Value!) : failure(Errors);
Implementing Results with Values
At this point, we are complete. All that remains is to put it all in action. Let’s go back and modify the DoSomething method to return a Result<T> value with a data type of string.
Result<string> DoSomething(string withSomething)
{
Console.WriteLine("Start the Process");
if(withSomething is not null)
{
Console.WriteLine("The Process finished");
return withSomething;
}
return new ArgumentNullException();
}
All we have done is modified the return type of the method and change the successful value return from “true” to the input string, withSomething. To use it, we go back to the Match method.
string ResultMessage = Result.Match(
(value) => "The operation succeeded",
(errors) => $"The operation failed with {errors.Count});
When to use Results
Like any pattern, it can be useful, and it can be abused. You do not need to use a Result as the return type for every method you write. It is best used in places where your next action or value is based on the binary outcome of an operation. An example might be an API where you either return data or an error message to the client.
Results can help with object creation as well. When constructing a complex object, the object may end up in a bad state depending on the input available. Use a Result in a factory or static create method to return a Result instead of a raw value. In this way, you can validate the state of the object being created and pass an Error instead of passing an invalid or null object. This can also avoid throwing exceptions inside a constructor.
Digital Results
So that was a fair amount of code to write. You’ll probably want that in a reusable library if you have more than one project that can benefit. The good news for you, is it is already available.
DigitalResults is a package available on NuGet that implements a modified version of the code illustrated here. The major difference is the use of Errors instead of Exceptions. That is a topic explained in the documentation and something I will address in a different post.
DigitalResults is available as a NuGet package called DigitalCaesar.DigitalResults. Use your package manager of choice to include DigitalResults in your project. The namespace you will need is DigitalCaesar.Results.
What do you think?
The example is overly simplistic to make it easier to understand and relay. Where this shines is in the UI layer where you are calling methods that may have layers of action behind them and you only want to do one of two things – return a value or share an error message. For example, and API that returns data will either return the data or show and error message to explain why the data is not available.
Download it today and try it. Let me know in the comments if it works for you or if you have any questions.
Curious about the code? Check out the Repo on GitHub. Give it a star and follow for updates. Have ideas or improvement? Feel free to contribute.
Credits
Thanks to Milan Jovanovic, Amichai Mantiband, and Harry McIntyre. Milan has a set of instructional videos on YouTube that first introduced me to the concept. Amichai and Harry both have great NuGet packages that offer similar functionality. However, each has variations in the approach that deviate from my needs.
Milan Jovanovic first introduced me to the concepts in his video series Clean Architecture & DDD Series on YouTube. He has a simple implementation that works quite well. There are also other key concepts in the series worth checking out.
Amichai has an implementation called ErrorOr that emphasizes the Error. ErrorOr makes the Value and Error list publicly available whereas I prefer to force the use of switch and match functions to avoid state issues. It also uses a list of Errors with a FirstOf concept to retrieve a sort of master error. It is a good concept but requires a bit of extra thought that I would rather avoid. However, I might consider incorporating similar logic to the ErrorCollection rather than directly to the Result.
Harry has an implementation called OneOf that allows for many possible outcomes beyond the simple binary success or failure. OneOf also exposes the Value and Errors publicly. It also declares all the possible Errors in the type allowing each to be handled independently. A great concept but it is limited to eight outcomes including the successful value result. It has the tendency to overcomplicate the consuming code with many outcomes versus keeping it a simple binary choice between success and failure.
Leave a Reply