Table of Contents

Result

Introduction

This packages provides a Result<TValue, TFailure> type for C#, spiritually similar to those available in Rust, Swift, Kotlin, C++ and basically every functional programming language under the sun.

Results are commonly used in scenarios where failure is anticipated can be handled gracefully by the caller. Examples include:

  • Input validation,
  • Parsing and conversion,
  • Invocation of external services,
  • Authentication and authorization,
  • and more ...

Result<TValue, TFailure> represents the result of a fallible operation as a first class value. A result can be in one of two states: "success" or "failure". Both states have an associated payload of type TValue or TFailure respectively.

Installation

NuGet Badeend.Result

dotnet add package Badeend.Result

Full API reference

Basic example

Create Result

public enum SignInError // Failures can be any type you want. For this example I chose a simple enum.
{
    InvalidCredentials,
    LockedOut,
}

/// <summary>
/// Attempt to log the user in and return the newly created user session.
/// </summary>
public Result<Session, SignInError> SignIn(string email, string password) // <--- Notice the return type.
{
    var user = FindUserByEmail(email);
    if (user is null || !user.VerifyPassword(password))
    {
        return SignInError.InvalidCredentials; // Error is implicitly wrapped with `Result.Failure(...)`
    }

    if (user.IsLockedOut)
    {
        return SignInError.LockedOut;
    }

    return user.CreateNewSession(); // Return value is implicitly wrapped with `Result.Success(...)`
}

Check Result

public async Task<ActionResult<Session>> PostSignIn(SignInRequest request)
{
    var result = SignIn(request.Email, request.Password); // The SignIn method from above.

    return result.State switch // Tip!: enable CS8509 & disable CS8524 for exhaustiveness checking.
    {
        ResultState.Success => Ok(result.Value),
        ResultState.Failure => BadRequest(result.Failure),
    };


    // Or alternatively, but more verbose:

    if (result.IsSuccess)
    {
        return Ok(result.Value);
    }
    else
    {
        return BadRequest(result.Failure);
    }
}

When should you use Results?

You can use Results when designing fallible methods where:

  • failures are part of the domain model and should therefore be part of the regular control flow. And/or:
  • the implementation is not in the position to decide whether failures are exceptional or not and you want to leave that up to the caller.

Choosing a failure type

It can be anything that describes the failure; an enum, a record, a list of validation messages, etc... Let your mind run free.

Keep in mind that the failure value should contain enough information for callers of your method to do something meaningful with it. If the caller has no other option than to propagate it up the callstack (recursively), then you might as well throw an exception.

One noteworthy case to watch out for is: Result<T, Exception>. There may be legitimate use cases for it, but it smells like it's trying to reinvent exception handling.

Results vs. exceptions

Results are not a general purpose replacement for exceptions. Keep using exceptions! Exceptions great for fatal errors, bugs, guard clauses and other non-recoverable errors. That being said, Results may be a suitable replacement for exceptions if you're currently catching exceptions for non-local control flow.

Results don't collect stack traces by design.

Ultimately, Results and Exceptions both have their pros and cons, and the choice depends on factors such as the specific requirements of your application, the level of robustness needed, and personal or team preferences.

Results vs. the bool Try***(out T t) pattern

Results are a generalization of the bool Try***(out T t) pattern. The Try pattern still works fine as long as the method:

  • has exactly one failure mode, and:
  • is not async. (out parameters don't work on async methods.)

Something to keep in mind when using the Try pattern: you are expected to provide a non-Try variant as well that throws the error instead of returning it.

The advantages of Results over Try methods:

  • Works with async methods.
  • Works with any number/kind of failures. (Technically you could use multiple out parameters for the additional error data, but that is frowned upon)
  • No API duplication. You only have to expose one Result-returning method. The caller decides whether failures should throw or not.

Results vs. nullables

The cookie cutter answer is: Nullables are about optionality, Results are about fallability.

However, especially when a method has only one failure mode, the distinction can become blurry. Consider the following snippet:

class MetaData
{
    ??? GetValueByKey(string key);
}

What happens when the key is not present? It depends on whether the caller is expecting the key to exist or not;

  • If the key is expected to exist, a non-existent key should be an error.
  • If the key isn't required to exist, a Nullable return type could suffice. Keep in mind that you then loose the distinction between: the key doesn't exist, and: the key exists but its value is null.

FYI, even the BCL isn't consistent in this regard. E.g.:

Are Results effectively Java's "checked exceptions"?

Yes! and: No!

Checked exceptions get a bad rep because they're implemented in only one mainstream language: Java. And Java's implementation of them has turned out to be horrible in practice. In Java, all exceptions are "checked" by default, which means that every exception must be explicitly marked for propagation in every method. This makes for an awfully laborious developer experience.

However, concluding that "all checked exceptions must therefore be bad" would be throwing the baby out with the bathwater. Checked exception are still useful: in moderation. Java just got their defaults wrong.

Using C# exceptions complemented with Results is the best of both worlds; by default you're never forced to unnecessarily check for errors, except for the few places where you explicitly opted-in to that (by using Result).

Why does this package exist?

There are already dozens of similar packages. Yet, surprisingly, none of them provide what I'm looking for:

  • No opinion on what is allowed to be a failure. In other words: I want the failure type to be parameterized (TFailure) without constraints. IMO, hardcoding the failure type to e.g. Exception or string completely defeats the purpose of using a result type in C#.

  • Just Result, nothing else. I'm not interested in a complete Functional Programming framework that introduces 20-or-so new concepts, pushes all code into lambdas and attempts to redefine what it means to write C#. Not because I don't like FP, but because the language & ecosystem (sadly 🥲) doesn't afford it.

  • "Native" C#. It should feel as if it is written by C# developers, for C# developers, for use in (existing) C# codebases. Or put differently: if such a type were to be added to the BCL, how would Microsoft design it?

Considered alternatives