Alternatives to the Exception pattern
In my last article, I wrote about the exception pattern. It has great benefits but also great performance implications. Because of these, and following Microsoft recommendations, I’ve also written that we should only use them for exceptional errors.
So what do we do when we have to handle an error? Let’s get to it.
The “Check and Do” pattern
Disclaimer: I’ll include this pattern just for completeness. I don’t encourage the use of this pattern because of its poor performance and verbosity.
The principle behind this pattern is to check the requirements before doing the action, hence the name. It has the merit of being easily understandable at the expense of being costly.
If I want to parse an integer from a string, I must :
- check if the string only includes digits
- parse the string
When implementing this, we will have to loop twice over the string and most likely have to double some operations. Overall, doubling the cost of an algorithm just to avoid having to throw an exception is a bad choice. But that’s still one way of doing it.
Another example is when we want to add a value to a dictionary without overwriting the potential existing value. Using the bracket syntax is not a suitable solution as it will overwrite the value.
To do so, we have to :
- check if the value exists or not
- add if needed Once again, we have a suboptimal algorithm.
You might know the TryAdd
method (doc here) which has been available in the framework since .NET Core 2.0. It is one example of the following pattern, the Try pattern.
The Try pattern
I like to see this pattern as an improved version of the previous one. It does the same things: first checking then doing, but it does that at the same time, in one method call. Thus, it’s more efficient.
The TryAdd
method of a dictionary (doc here) is an example of a write action: there is also a read part that we’ll see a bit later.
This method will do the action and will return a boolean to signal whether or not the action was properly done. If we get true
then we know that the key/value was added. If we get false
the key already exists and thus the value wasn’t changed. Having this information, we can handle our business case.
What about reading value, like getting the value of a key that may not exist?
A specific method exists for that, the TryGetValue
(doc here). This method has existed since .NET Framework 2.0 (back in 2005!) and uses a very old feature (from the initial release of .NET) the out
parameters. The out
parameter allows the developer to assign a value to a variable by passing it to a method. It’s been improved in C# 7.0 to allow both declaration and assignment at the same time, like so:
var dictionary = new Dictionary<int, string>();
if(dictionary.TryGetValue(42, out string value)) // value is both declared and assigned
{
Console.WriteLine($"The key 42 exists with value: {value}");
}
Thanks to the returned boolean, we can safely call Console.WriteLine
without taking the risk to throw a KeyNotFoundException
.
The out
parameter is quite neat and was necessary back then: The “new” (introduced in C# 7.0) tuple notation (ex: (bool, int)
) leverages a new structure the ValueTuple
, which is a value type instead of the “old” Tuple
that was a reference type. There is a lot to say about both value and reference type, this a not the subject of this blog post (may be a future one, though). The big difference between ValueTuple
and the regular Tuple
is that the first one is stack allocated whereas the latter is heap allocated. This means that every time you create a new Tuple
, it has to be regularly checked up by the garbage collector (GC), and destroyed when it becomes unused. This puts pressure on the GC, and overall it’s bad performance-wise when it happens too often. Of course, that’s especially true for hot paths.
The Result pattern
In its most basic form, this pattern is more or less the same as returning the tuple (bool, ReturnValue)
. If the first item is true
, then ReturnValue
is provided if not, it’s null. Of course, the return value’s type changes depending on the method, so it has to be a generic type:
public struct Result<T>
{
public bool IsSuccessFul { get; private set; }
public T? Value { get; private set; }
public Result(T Value)
{
IsSuccessful = true;
Value = Value;
}
public Result()
{
IsSuccessful = false;
}
}
There is no official implementation of this pattern, so you’ll have to make your own or use some open-sourced ones. Although most of them are based on classes, I think that it’s suboptimal: it produces allocations every time the method is called, which puts additional pressure on the GC for no reason. A small and short-lived bunch of data like this is a typical example where a struct
is more appropriate.
The first two patterns (“Check and do” and “Try”) are a bit verbose and require conditional blocks. The Result pattern works around this drawback by leveraging a fluent API. This kind of API lets you chain method calls to apply them successively to an initial value, such as Linq does with IEnumerable<T>
. However, in the case of this pattern, the next link will only be executed if the previous one was successful. Instead of the Select
as name for the method, let’s name it Then
.
public static Result<U> Then<T, U>(this Result<T> result, Func<T, U> fn)
{
return result.IsSuccessFul
? new Result<U>(fn(result.Value))
: new Result<U>();
}
As you can see, fn
will only be executed if the result is successful. And that’s the basics of the Result pattern. From there, there is a whole tree of possibilities for augmentations. For instance, you could:
- embed an error value, to contextualize the erroneous case
- add additional extension methods:
Or
method that does the opposite ofThen
and allows you to handle a fallback valueSwitch
/Match
to return one value in case of error and another in case of successTry
to wrap methods that throw exceptions and return a result instead- …
Comparison between the patterns
Let’s build the same example with the different patterns we saw: We have two methods One
and Two
that have to handle an erroneous case. Two
takes the result of the first one as a parameter. If one of them fails we have to return a fallback value.
Exception pattern
try
{
var value = One();
return Two(value);
}
catch(Exception)
{
return fallbackValue;
}
Check and Do pattern
if(!CanDoOne())
{
return fallbackValue;
}
var value = One();
if(!CanDoTwo(value))
{
return fallbackValue;
}
return Two(value);
Try pattern
if(!TryOne(out var value) && !TryTwo(value, out var result))
{
return fallbackValue;
}
return result;
Result pattern
return One().Then(x => Two(x)).Or(fallbackValue);
These examples conclude this blog post. What’s your favorite? Please, let me know if I made mistakes and/or if any passages are unclear. Don’t hesitate to react or comment below, I will gladly reply to your comments.