DEV Community

Eesaa Philips
Eesaa Philips

Posted on

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

Top comments (12)

Collapse
 
pitming profile image
pitming

Everytime I see the suggestion of using Resultobject instead Exception. I postulate that they don't know the difference between the normal flow of a program and the abnormal (error, exception, ...) flow.
Here in your exemple it's perfectly normal that when searching for something, you don't have a hit. So ofc you should never throw a NotFoundException here. But having a ResultObject here is wrong because when you search something and don't find it, you just return null. But if, while searching your object, you connection to the database vanish, then it makes perfectly sense to throw an exception.
Then you can argue when should I use that kind of pattern ? This pattern makes a lot of sense in command/event programing where, for a command, you have 2 kind results: CommandSuccceed and CommandFailed. Here you keep the exception for "transport" problem and put the failure object for business information. You can see that kind of usage in Microsoft.CognitiveServices for example

Collapse
 
ephilips profile image
Eesaa Philips • Edited

I understand the fundamental difference between normal flow and the abnormal.
This example is admittedly a bit unrealistic: If the object is not found, you can just return null.
Like I mentioned in the article, you can return an Exception object in the result instead of throwing it if you want custom Exception types. After using rust, I found that using a Result with a match function forces you to handle both success and failure and leaves you many options in handling that error. Result is malleable and you can sculpt it to fit your system's needs.
You seem like a seasoned developer based on your profile, I'm sure you've come across codebases with tons of try-catch blocks for normal flows that could be better handled with something as simple as the solution I offered. And even for abnormal flows, you can have result types with custom types to differentiate those if you willed. Or if it's fatal, just let the exception be thrown and catch it in the middleware in those special cases.
I personally never fiddled with CQRS but what you said about it being ideal with commands and events makes sense.

Collapse
 
forreason profile image
Julian B.

I dont understand your argument. An empty search result would totally fit this pattern:

{
    success: true,
    searchresults: [],
    error: null
}
Enter fullscreen mode Exit fullscreen mode

your failed database connection also fits in:

{
    success: false,
    error: "the connection to the database timed out!"
}
Enter fullscreen mode Exit fullscreen mode

Would mainly use it in error prone tasks such as io, sockets and so on.

Collapse
 
romb profile image
Roman • Edited

last year I had an opportunity to try this approach to error handling on one of short-term projects

In theory, the author is right: less resource-consumable and much faster with result types approach. In practice, the approach is applicable only to pretty "thin" layers, e.g. DAL or BL (moreover, since different layers have different models, you'll have to convert (or at least explicitly pass-through) these error types (models) on layer boundaries). Otherwise, you'll have to write tedious "if" statements and convert results from one type to another, which is difficult to maintain.

So for myself I have decided to go with the result-based approach only on small projects (with "thin" layers or even one-layer app), otherwise, traditional exception-based approach is a way to go.

Additionally, lots of MS or 3P libraries throw exceptions, so handling exceptions is almost unavoidable. Sure, it's not the same as throwing, but anyway, a call stack in such cases may be more useful, then just exception message.

Collapse
 
ephilips profile image
Eesaa Philips

Yeah, I agree. If it's within the normal flow of the application, a result type is expedient.
For example, I'm working with a quiz-taking platform; when the user wants to start the quiz, it may be deleted just then so I return an error "Quiz was deleted", it's due date may have passed after he opened the page client side : "Due date has passed", etc.
In this case, I just convert the result from the service layer to an http response at the controller. If it's an error I return a bad request with the message, otherwise I return the result.

Collapse
 
alexzeitler profile image
Alexander Zeitler • Edited

I like this solution. Can I use it for methods which don't return a value (void)?
Maybe something like F# Unit?
Or similar to MediatR Unit.

Collapse
 
ephilips profile image
Eesaa Philips

Hello, Alexander.
I use Unit. I have a global variable imported as unit so I can just do something like this:

  public async Task<Result<Unit, string>> UpdateQuiz(ClaimsPrincipal claims, int quizId)
    {
        // check if the quiz has been added to a course
        var assignments = await _quizRepo.GetGoogleClassroomAssignments(quizId);
        if (assignments.Count == 0)
        {
            return unit;
        }
        ... // more logic here
Enter fullscreen mode Exit fullscreen mode
Collapse
 
alexzeitler profile image
Alexander Zeitler

I tried it with MediatR Unit and it seems to work nicely.

Collapse
 
wahidustoz profile image
Wahid Abduhakimov

I would argue the opposite. The standard error handling in .NET is using exceptions. Returning types as errors is so wrong in so many ways. The biggest issue is, it swallows actual error messages from telemetry.

You can argue that you can try catch the error, log it and then return the result type. What's the point of having result types then?

Collapse
 
ephilips profile image
Eesaa Philips • Edited

I never argued to try catch an error then return a result type. Languages with no exception throwing like go and rust handle this by simply using custom error types. You could use different classes to represent different error types in your result or just return an exception in your result instead of throwing it like I mentioned in the article. This is not a silver bullet but I personally find it much more logical than try catch finally blocks.

If a fatal exception happens, just let it propogate and be caught by some middleware but that should only ever happen for errors you can't really handle such as the DB connection being closed

Collapse
 
wahidustoz profile image
Wahid Abduhakimov

What I prefer is simply let the caller take care of data integrity. If methods dont receive valid data, just throw an exception. That way you dont lose stack trace. Caller should handle the exceptions it wants and let other exceptions go through.

Finally, you can have a middleware to catch the remaining errors.

Collapse
 
romb profile image
Roman

Totally agree, e.g. @forreason 's code above

{
    success: false,
    error: "the connection to the database timed out!"
}
Enter fullscreen mode Exit fullscreen mode

implies catching an exception, and putting its message into IResult.Message property
so no point in "struggling" with exceptions replacing them with IResult when you have try/catch'es

it's better to admit and accept exceptions :)

also agree with @pitming, people often don't distinguish between normal and abnormal flows trying to blame those who uses exceptions in implementing business logic with exceptions.