DEV Community

Better error handling in C# with Result types

Eesaa Philips on June 10, 2023

The Problem Exceptions are slow, cumbersome, and often result in unexpected behavior. Even the official Microsoft docs tell you to limit...
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.