The Result Pattern

Handling exceptions in software development is a crucial aspect that can significantly impact the maintainability and performance of your code. While traditional...

Written by Bala · 3 min read >

Handling exceptions in software development is a crucial aspect that can significantly impact the maintainability and performance of your code. While traditional methods like throwing exceptions are common, they come with their own set of challenges. This blog will explore an alternative approach to managing errors using the Result Pattern or also known as the Result Type, focusing on C# 12 and .NET 8. In this blog I will discuss the drawbacks of exceptions and how the Result Pattern can offer a more maintainable and performant solution.

Usually in the domain layer, validations are handled either by throwing exceptions or returning result objects containing error messages. Throwing exceptions has the advantage of capturing stack traces, which aids in debugging by providing contextual information. However, this approach can lead to a proliferation of custom exception classes, making the codebase cumbersome. Additionally, exceptions have a performance cost due to the overhead of creating and throwing them.

Here’s an example of using exceptions for validation in a domain service method

public void FollowUser(Guid userId)
{
    if (userId == CurrentUserId)
    {
        throw new CannotFollowSelfException("You cannot follow yourself.");
    }

    // Other validation and business logic
}

While this approach is straightforward, it requires consumers to handle these exceptions appropriately, which might not always be ideal.

An alternative to using exceptions is the Result Pattern. This pattern involves returning a result object that encapsulates the success or failure of an operation, along with any associated errors. This approach improves code readability and maintainability by reducing the number of exception classes and making error handling more explicit.

Let's start with a simple implementation where methods return strings representing error messages.

public string FollowUser(Guid userId)
{
    if (userId == CurrentUserId)
    {
        return "You cannot follow yourself.";
    }

    // Other validation and business logic

    return string.Empty;
}

Although more performant, this approach is less maintainable. To improve on this, we can introduce a custom error type as follows.

public class Error
{
    public string Code { get; }
    public string Description { get; }

    public Error(string code, string description = null)
    {
        Code = code;
        Description = description;
    }
}

Thereafter we could update the FollowUser method shown above as follows.

public Error FollowUser(Guid userId)
{
    if (userId == CurrentUserId)
    {
        return new Error("Followers.CannotFollowSelf", "You cannot follow yourself.");
    }

    // Other validation and business logic

    return Error.None;

We could thereafter further improve the Result type so that it encapsulates both success and failure, like shown below

public class Result
{
    public bool IsSuccess { get; }
    public Error Error { get; }

    private Result(bool isSuccess, Error error)
    {
        IsSuccess = isSuccess;
        Error = error;
    }

    public static Result Success() => new Result(true, Error.None);
    public static Result Failure(Error error) => new Result(false, error);
}

We could thereafter update the FollowUser method to return the Result type as depicted below.

public Result FollowUser(Guid userId)
{
    if (userId == CurrentUserId)
    {
        return Result.Failure(new Error("Followers.CannotFollowSelf", "You cannot follow yourself."));
    }

    // Other validation and business logic

    return Result.Success();
}

Result Type with Generic

I also came across a variant of the Result Pattern/Type implementation, where the Result type is implemented either as a Struct or a Class with generic type parameters to handle different types of results and errors. Here's one.

public struct Result<TValue, TError>
{
    public TValue Value { get; }
    public TError Error { get; }
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;

    private Result(TValue value, TError error, bool isSuccess)
    {
        Value = value;
        Error = error;
        IsSuccess = isSuccess;
    }

    public static Result<TValue, TError> Success(TValue value) =>
        new Result<TValue, TError>(value, default, true);

    public static Result<TValue, TError> Failure(TError error) =>
        new Result<TValue, TError>(default, error, false);

    public void Match(Action<TValue> onSuccess, Action<TError> onFailure)
    {
        if (IsSuccess) onSuccess(Value);
        else onFailure(Error);
    }
}

Here’s how you can use the Result Type in a method that performs a validation:

public Result<Movie, string> CreateMovie(Movie movie)
{
    if (string.IsNullOrEmpty(movie.Title))
    {
        return Result<Movie, string>.Failure("Movie title cannot be empty.");
    }
    if (movie.ReleaseDate > DateTime.UtcNow)
    {
        return Result<Movie, string>.Failure("Movie release date cannot be in the future.");
    }

    // Logic to save the movie
    return Result<Movie, string>.Success(movie);
}

Consumers of this method can then handle the result explicitly

var result = CreateMovie(new Movie { Title = "New Movie", ReleaseDate = DateTime.UtcNow.AddYears(1) });

result.Match(
    movie => Console.WriteLine($"Movie created: {movie.Title}"),
    error => Console.WriteLine($"Error: {error}")
);

Benefit of the Result Pattern

Maintainability - Traditional error handling often involves creating many custom exception classes for different error states, leading to a bloated codebase. The Result Pattern encapsulates errors within a single result type, using specific error codes and messages. This reduces the number of classes, simplifying the codebase and making it easier to manage and maintain.

Performance - Throwing and catching exceptions involves significant overhead, affecting application performance, especially when exceptions are used for non-exceptional scenarios like validation errors. The Result Pattern avoids this by using regular control flow constructs, encapsulating error states within the result type without the costly exception handling mechanism, leading to more efficient code execution.

Readability - Exceptions can obscure code logic, making it harder to follow. The Result Pattern makes error handling explicit by clearly delineating success and failure outcomes within the result type. This clarity enhances code readability, making the flow of error handling easier to understand and follow.

Testing -Unit testing with exceptions often requires testing for thrown exceptions, which can be cumbersome. The Result Pattern simplifies this by allowing tests to check specific error codes and messages within the result type. This makes writing precise and clear tests easier, directly inspecting the result type to determine success or specific errors, leading to more straightforward and maintainable test cases.

References

There are quite a few variants to the aforementioned Result Type or Pattern, which you may want to look up as further references.

Adralis Result Type implementation - This is a separate Nuget package initiated by Steve Smith and maintained by the community. IMHO this Nuget is very comprehensive and covers most of the stuff which I have discussed above.

May be in a separate post I will have an elaboration on how this Nuget could be used in a ASP.NET Core Web API application.

C# Language Core Extensions Developed by a group of .NET enthusiasts also has a nuget package of a similar implementation of the Result Pattern or Type.