Skip to main content

Command Palette

Search for a command to run...

Avoiding Result Pitfalls

Updated
7 min read

Sadly, C# does not ship with a Result<T> as part of the BCL. Even sadder, our friends lucky enough to work in F# have a really good one, so the .NET IL is clearly capable of it.

This hasn't prevented many an ambitious programmer from attempting to write their own Result<T> type, and why wouldn't you? Exceptions are unexpected, and expensive. A type that communicates to the programmer that failure is a possibility and that they should be prepared to handle it will only ensure good coding practices, right? Sadly, this isn't always the case.

Many codebases end up littered with Result<T> types and throw many exceptions. The object causes friction and is most often ignored. This leaves developers with a bad taste in their mouth about the Result<T> type. Some of this is avoidable, and some of this, sadly represents a shortcoming in the C# language itself.

By understanding where developers go wrong, and where they language falls short, we can create a more robust Result<T> type that stands a much better chance of justifying the additional complexity.

Where C# Falls Short: No Safe Unwrap

Some languages, like Rust, provide an elegant way to unwrap Result<T> types. In the case of Rust, the ? operator will unwrap a Result<T> and return the value. If the Result<T> represents an error, the function immediately early returns the error. Thus, any function that uses the ? operator in Rust, must itself return a Result<T> type.

fn read_file_contents(filename: &str) -> Result<String, io::Error>
{
    // Open the file, using `?` to propagate errors
    let mut file = File::open(filename)?;

    let mut contents = String::new();
    // Read the file contents into a string, using `?` to propagate errors
    file.read_to_string(&mut contents)?;
    Ok(contents) 
}

This will feel very familiar to C# developers who have a similar unwrap mechanic for Task<T>: async/await. the await keyword unwraps a task, and any function which uses await must be marked async and itself, return a task.

public async Task<string[]> ReadFileContents(string path)
{
    // read all lines using `await` to unwrap an asynchronus function
    var lines = await File.ReadAllLinesAsync(path);
    return lines;
}

Because a Result<T> type is not part of the BCL, there's no reason (or good way really) to add a similar operator to unwrap a Result<T> in C# and propagate any errors. Sadly, the following is not valid C#

public Result<string> GetUsername(Guid userId)
{
    // use `?` to propogate errors
    var user = _db.Find<User>(userId)?
    return user.Name;
}

Where Developers Go Wrong: The Unsafe Unwrap

In an effort to make their Result<T> types easier to use, many developers will add an unsafe unwrap. Usually this takes the form a property called Value which simply throws if the Result<T> represents a failure. The following examples for the rest of this article are using C# 12 with nullable enabled, and all warnings treated as errors.

// A simple Result<T> with an unsafe unwrap called `Value`
public class Result<T>
{
    private readonly T? _value;
    private readonly Exception? _exception;
    private Result(T? value, Exception? exception)
    => (_value, _exception) = (value, exception);

    public static Result<T> Success(T value) => new(value, exception: null);
    public static Result<T> Failure(Exception ex) => new(value: null, ex);

    // The unsafe unwrap
    public T Value => _value ?? throw ex;
}

If the option to simply "get the value out" of a Result<T> is there, developers will take it. They will take it and the code will work, until one day it doesn't. The entire reason for switching to a Result<T> type is lost. You're carefully wrapping return values in a type that prepares for failure, only for the calling code to assume success anyway.

What can we do?

Understanding the language shortfalls, and the pitfalls of an unsafe unwrap, is it possible to make a Result<T> worth using in C#? That answer to that will always be subjective, but I believe we can do better than the example above.

Match/Switch Functions

How do you best get the value out of a Result<T>? One way is to flip the question, or "How do you best inject behavior into a Result<T>" By providing Match/Switch functions we can require that calling code provide a path forward in both the case of a success or a failure. Here is an implementation of this, and an example of its usage

public record Error(string Message);

public class Result<T>
{
    private readonly T? _value;
    private readonly Error? _error;

    private Result(T? value, Error error)
    => (_value, _error) = (value, error);

    public static Result<T> Success(T value) => new(value, null);
    public static Result<T> Failure(Error error) => new(null, error);

    public TOut Switch<TOut>(
      Func<T, TOut> success,
      Func<Error, TOut> error)
    => _value is not null
        ? success(_value)
        : error(_error!);
}

// Usage
Result<User> userResult = GetUser(userId);
string userName = userResult.Switch(
    success: user => user.Name,
    error: err => $"Unable to find user: {err.Message}");

return userName;

This doesn't have an unsafe unwrap, but it can be a little awkward in an imperative codebase, which describes quite a bit of C# code. To address this, we can rely on subclasses and pattern matching

Subtypes and Pattern Matching

C# pattern matching provides a powerful and straight forward way to ask about the runtime type of an object and capture a typed reference to that object. This makes it easy for use to check for a successful Result<T> with a simple if statement and use the successful value in a compile checked way.

public record Error(string Message);
public abstract class Result<T>
{
    public static Result<T> Success(T value) => new Success(value);
    public static Result<T> Failure(Error error) => new Failure(error);
}

public class Success<T> : Result<T>
{
    public T Value { get; }
    internal Success(T value) => Value = value;
}
public record Failure<T> : Result<T>
{
    public Error Error { get; }
    internal Failure(Error error) => Error = error;
}

// Usage
Result<User> userResult = GetUser(userId);
if (userResult is Success<User> success)
{
    return success.Value.Name;
}

Unfortunately, there isn't a great way to tell the compiler that if a Result<T> instance isn't a Success<T> type, then it must be a Failure<T> type. The lack of discriminated unions is why we're in this pickle to begin with. Still however, we can press on and refine this type further

Implicit operators

We can clean up our static factory methods with implicit conversions, this if a function's return type is Result<T> we can simply return a T or an Error

public abstract class Result<T>
{
    public static implicit operator Result<T>(T value)
    => new Success(value);

    public static implicit operator Result<T>(Error error)
    => new Failure(error);
}

// Usage
public static Result<decimal> Divide(decimal numerator, decimal denominator)
{
    if (denominator == 0) return new Error("Cannot divide by zero");
    return numerator / denominator;
}

Adding a Safe Unwrap

Next let's try and add a safe unwrap (or at least to most elegant one that C# will let us) We want failure to be the true condition so we can early return the error. We'll use the nullable branch analysis to indicate to the client code when out parameters are or are not null.

public abstract class Result<T>
{
    // Omitted

    public abstract bool UnwrapFailed(
        [NotNullWhen(true)]
        out Error? error,
        [NotNullWhen(false)]
        out T? value);
}


public class Success<T> : Result<T>
{
    public override bool UnwrapFailed(
        [NotNullWhen(true)]
        out Error? error,
        [NotNullWhen(false)]
        out T? value)
    {
        error = null;
        value = Value;
        return false;
    }
}
public record Failure<T> : Result<T>
{
    public override bool UnwrapFailed(
        [NotNullWhen(true)]
        out Error? error,
        [NotNullWhen(false)]
        out T? value)
    {
        error = Error;
        value = null;
        return true;
    }
}

// Usage
public Result<string> AssembleGreeting(Guid greetingId, Guid nameId)
{
    Result<string> greetingResult = LoadGreeting(greetingId);
    Result<string> nameResult = LoadName(nameId);

    if (greetingResult.UnwrapFailed(out var error, out var greeting))
        return error; // error is not null here
    if (nameReuslt.UnwrapFailed(out error, out var name))
        return error // error is not null here

    // neither greeting or name are null here
    return $"{greeting}, {name}!";
}

Conclusion

C# is a high ceremony language, and there are limits to how succinctly we can express certain concepts and structures that come naturally to other languages like Rust or F#, but with this in mind we can still craft a result that achieves its goal: forcing the calling code to deal with any potential errors at compile time.

Here are some fun exercises when crafting your own result types:

  • Add a map/select function

  • Extend the map/select function to automatically flatten (Result<Result<T>> becomes Result<T>)

  • Add a way for a Result<Task<T>> to be awaited returning Result<T>

  • Add an equality operator which respects referential transparency

    • Two Result<int> objects which both contain the same number should be equal

    • A Result<int> which is a Success<int> that holds 5 should be equal to an int which holds 5