Better error handling in C# with Result types

ephilips

Eesaa Philips

Posted on June 10, 2023

Better error handling in C# with Result types

The Problem

Exceptions are slow, cumbersome, and often result in unexpected behavior. Even the official Microsoft docs tell you to limit your use of exceptions.
Most of the time, you want to handle both the success and failure cases without allowing the exception to propagate. You might be wondering "if I won't use exceptions, how can I tell the caller function that something went wrong?". This is where the result type comes in.

The Result type

When our function needs to represent two states: a happy path and a failure path, we can model it with a generic type Result <T, E> where T represents the value and E represents the error. A function that gets a user could look like this:

public async Result<User, string> FindByEmail(string email) {
    User user = await context.Users.FirstOrDefaultAsync(
        u => EF.Functions.Like(u.Email, $"%{email}%"));
    if(user is null) {
        return "No user found";
    }
    return user;
}
Enter fullscreen mode Exit fullscreen mode

You would call the function like this:

[HttpGet("{email}")]
public async Task<ActionResult<User>> GetByEmail(string email)
{
    if(string.IsNullOrEmpty(email)) {
        return BadRequest("email cannot be empty");
    }
    Result<User, string> result = await FindByEmail(email);
    return result.Match<ActionResult<User>>(
        user => Ok(user),
        _ => NotFound());
}
Enter fullscreen mode Exit fullscreen mode

If you don't want to return strings for errors, but instead a different type, you can define those classes/structs and return them or use the existing Exception types for your errors. Returning exceptions is fine, throwing them is what's costly.

Here is the code for the result type:

public readonly struct Result<T, E> {
        private readonly bool _success;
        public readonly T Value;
        public readonly E Error;

        private Result(T v, E e, bool success)
        {
            Value = v;
            Error = e;
            _success = success;
        }

        public bool IsOk => _success;

        public static Result<T, E> Ok(T v)
        {
            return new(v, default(E), true);
        }

        public static Result<T, E> Err(E e)
        {
            return new(default(T), e, false);
        }

        public static implicit operator Result<T, E>(T v) => new(v, default(E), true);
        public static implicit operator Result<T, E>(E e) => new(default(T), e, false);

        public R Match<R>(
                Func<T, R> success,
                Func<E, R> failure) =>
            _success ? success(Value) : failure(Error);
    }
Enter fullscreen mode Exit fullscreen mode

The implicit operators allow you to return a value or error directly. For example, return "err" can be used instead of return new Result<User, string>.Err("err"). It will automatically convert it to a result type for you.

Performance comparison

I wrote a benchmark that compares returning a result with throwing an exception. The benchmark compares the identical functions if they fail 0% of the time, 30% of the time, 50% of time, and 100% of the time.
Here are the results:
Image description

What we care about here is the mean. You can see that returning the result directly and matching outperforms exception handling as exceptions get thrown more often.
In reality, your function will probably not throw exceptions 50% of the time but this is just a benchmark to illustrate how slow exceptions can be if used often.

This is the benchmark code:

    [Params(0, 30, 50, 100)]
    public int failPercent;

    [Benchmark]
    public int[] result() {
        int[] res = new int[100];
        for(int i = 0; i < 100; i++) {
            res[i] = getNum_Res(i < failPercent).Match<int>(
                    n => n + 10,
                    err => 0);
        }
        return res;
    }

    [Benchmark]
    public int[] exception() {
        int[] res = new int[100];
        for(int i = 0; i < 100; i++) {
            try {
                res[i] = getNum_Exception(i < failPercent) + 10;
            }catch(Exception) {
                res[i] = 0;
            }
        }
        return res;
    }

    public Result<int, string> getNum_Res(bool fail) {
        if(fail) {
            return "fail";
        }
        return 1;
    }

    public int getNum_Exception(bool fail) {
        if(fail) {
            throw new Exception("fail");
        }
        return 1;
    }
Enter fullscreen mode Exit fullscreen mode

Closing Thoughts

Throwing less exceptions means writing less lines of code, and handling errors more efficiently. Remember, "The best code, is no code at all". Have a good day :)

💖 💪 🙅 🚩
ephilips
Eesaa Philips

Posted on June 10, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related