RSS Amplifier

Zest · Apr 15, 2024

Exceptions vs. Result Pattern in C#

0
Sign in to vote or save

Haseeb Ahmed · Zest

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!

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 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

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:

  1. An exception occurs

  2. CLR creates the exception object and populates it with relevant information e.g. the error message, stack trace, etc.

  3. CLR unwinds the stack to search for a suitable catch block that can handle this exception.

  4. If no catch block is found, CLR terminates the program and displays an error message to the user.

  5. If a suitable catch block is found, control is transferred to it for execution.

  6. 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 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.

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 custom Error type

    • IsSuccess 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}!");
    }
}

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.

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.

Sharing is caring! Share now to help others join a community of developers passionate about engineering.

Share

PS. The complete solution with the benchmarks is available on GitHub.

Read the original on zestbyhaseeb.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.