Exceptions vs. Result Pattern in C#
To throw or not to throw? The shift in C# programming paradigm from exceptions to the result pattern.
Exceptions have been used for controlling the code flow in C# for as long as I can remember. Most Microsoft and 3rd party libraries still use exceptions. However, recently the community has started adopting a new functional programming concept called Result pattern.
In this blog post, we’ll look at exceptions and the result pattern. We’ll also implement a very naive Result type. I’ll also show how and which Result type to use in your production applications. Lastly, we’ll compare both and see why the Result pattern might be a better approach. Let’s get started!
Exceptions
In C#, exceptions are types directly or indirectly inherited from the System.Exception
class. They are created using the throw
keyword and exit the current code flow in case of an error. The catch
block defines an exception handler that catches the exception and has the logic to leave the application in a stable state.
Below is an example code snippet that demonstrates the use of exceptions.
internal class ExceptionExample
{
public static void Main()
{
try
{
Console.Write("Enter your name? ");
var name = Console.ReadLine();
Console.WriteLine(SayHello(name));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private static string SayHello(string? name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException(nameof(name));
}
return $"Hello {name}!";
}
}
ThrowIf Pattern
ThrowIf is a technique for throwing exceptions and has been around for a while. It is used when you want to put a certain set of preconditions into a single reusable method but is not limited to that. It can also be used to reduce the boilerplate if-else code for frequently used checks e.g. the null check.
Unfortunately, I have not seen this pattern used that much but starting with .NET 6, Microsoft included some basic ThrowIf methods on the framework exceptions itself. Let’s refactor the SayHello()
method in the example above to use one of the built-in ThrowIf methods.
private static string SayHello(string? name)
{
ArgumentNullException.ThrowIfNullOrEmpty(name, nameof(name));
return $"Hello {name}!";
}
In the example above, we can see that the ThrowIf pattern:
Reduces boilerplate and makes the code cleaner
Is (arguably) more readable
Can centralize more complex logic used in multiple places
Exceptions are expensive!!
Despite their common usage, one thing that a lot of engineers are not aware of is that throwing exceptions is expensive! To understand why, let’s look at what happens behind the scenes when an exception is thrown:
An exception occurs
CLR creates the exception object and populates it with relevant information e.g. the error message, stack trace, etc.
CLR unwinds the stack to search for a suitable
catch
block that can handle this exception.If no
catch
block is found, CLR terminates the program and displays an error message to the user.If a suitable
catch
block is found, control is transferred to it for execution.If a suitable
finally
block is present, control is transferred to it for execution.
In the steps above,
Object creation (step #2) is an expensive operation because it requires constructor execution and memory allocation. Moreover, collecting state information such as the stack trace is also an expensive operation because it can potentially require accessing and storing large amounts of data.
Stack unwinding (step #3) can also be a computationally expensive operation if the call stack is deep because the CLR has to inspect stack frames for suitable
catch
block.
Here is a screenshot of a very basic benchmark that I ran locally on my laptop.
It shows that exceptions can be 27,000 times slower 😱. However, the results of this benchmark should not be taken at face value. I say that because, in a real-world scenario, not every execution would result in an exception but still the difference is significant enough to warrant caution when working with exceptions.
Result Pattern
Result pattern is a functional programming concept. It encourages returning a Result object (instead of throwing exceptions) to control the code flow. This object represents either the success or the error state. The calling code handles both scenarios making it a very expressive style of coding.
Implementation
Result objects are usually built into functional programming languages e.g. Rust but C# does not have a built-in type. Let’s start by implementing a very naive Result type.
public record Result
{
public string? Data { get; }
public Exception? Ex { get; }
public bool IsSuccess { get; }
private Result(
bool isSuccess,
string? data = null,
Exception? ex = null)
{
Data = data;
Ex = ex;
IsSuccess = isSuccess;
}
public static Result Error(Exception ex) => new(false, ex: ex);
public static Result Ok(string data) => new(true, data);
}
In the example above, the Result
type has:
Three properties: Data, Ex and IsSuccess
Data is the value that should be returned in case of no errors
Ex is any error that occurred during a method execution. We’re using the built-in
Exception
class for ease and interoperability but it could also be a customError
typeIsSuccess is true if no errors occurred or false otherwise
Two static helper methods: Ok() and Error()
Ok() method is used to return a Result object with success data
Error() method is used to return a Result object with an error
Private constructor, so that a Result object can only be constructed with the right combination of properties i.e. Data + IsSuccess = true or Ex + IsSuccess = false.
Now, let’s rewrite the exceptions example using the Result pattern.
internal class ResultPatternExample
{
public static void Main()
{
Console.Write("Enter your name? ");
var name = Console.ReadLine();
var result = SayHello(name);
Console.WriteLine(result.IsSuccess ? result.Data : result.Ex!.Message);
}
private static Result SayHello(string? name)
{
if (string.IsNullOrEmpty(name))
{
return Result.Error(
new ArgumentNullException(nameof(name))
);
}
return Result.Ok($"Hello {name}!");
}
}
Result type for Production
Now that we understand how the Result pattern works under the hood, I would not recommend using the Result type we created above in Production applications. Luckily enough, there is an amazing NuGet package called LanguageExt.Core that — among other great things — has an excellent Result type that we can use. Once again, let’s refactor our example above to use the new Result type.
using LanguageExt.Common;
namespace ExceptionsAndResultPattern
{
internal class ResultPatternExampleV2
{
public static void Main()
{
Console.Write("Enter your name? ");
var name = Console.ReadLine();
Console.WriteLine(
SayHello(name).Match(
(val) => val,
(ex) => ex.Message
));
}
private static Result<string> SayHello(string? name)
{
if (string.IsNullOrEmpty(name))
{
return new Result<string>(
new ArgumentNullException(nameof(name)));
}
return $"Hello {name}!";
}
}
}
The final code is more or less the same, the only two new concepts that you can see above are:
The
Match()
method: It is a functional way of handling all the possible outcomes of a result which in our case is either success or failure.Implicit result conversion: Unlike the original implementation where we returned
Result.Ok()
for success, this Result type implements implicit conversion which is why when returning a successful result we can simply return the actual value and the CLR will take care of converting it into the appropriate Result object.
Conclusion
Even though exceptions are a familiar concept to most C# developers, the Result pattern might be a better approach for controlling the code flow. That is because:
Exceptions are meant to be used for exceptional cases only e.g. failure in I/O operations or unexpected network issues, etc. On the other hand, Result objects should be used for expected errors e.g. handling success or failure based on some business rules.
Theoretically, throwing exceptions can be 27,000 times slower in performance than returning Result objects.
Handling exceptions is optional whereas the Result pattern requires explicitly handling success and failure, making code linear and predictable.
Using the Result type in LangaugeExt.Core, the friction between Result objects and 3rd party library exceptions can be reduced as well.
PS. The complete solution with the benchmarks is available on GitHub.