Don't throw exceptions in C#. Do this instead

preview_player
Показать описание

Hello everybody I'm Nick and in this video I will show you how you can potentially replace some of your exception throwing in your C# application with a differnent way of handling bad state. It is a concept that is native in other languages like Rust and F#, and it will eventually make its way into C# too but for now, here is the workaround.

Don't forget to comment, like and subscribe :)

Social Media:

#csharp #dotnet
Рекомендации по теме
Комментарии
Автор

I don't throw my exceptions. I instead keep them for myself in a safe place when they can feel that they are wanted, and feel warm and cozy.

Tal__Shachar
Автор

I want a two hour video of Nick explaining what monads are, gradually becoming more and more wild-eyed and frantic as it progresses

jackkendall
Автор

I've tried this approach in a production system. The code rapidly becomes unwieldy because you have to check the result at every exit and if the return type of the calling method is different you have to transform that result as well. The deeper the call stack, the more overhead / boiler plate / noise you add to the code purely to avoid throwing an exception. I don't think the maintenance overhead is worth the trade-off unless you desperately need the performance. I went with throwing useful exceptions and mapping them to problem details in middleware. It was easy to understand and follow and even easier to use and maintain the surrounding code in the call stack.

randomcodenz
Автор

This brings us back to pre-exception times where error conditions had to be handled on every level of the call hierarchy. It reminds me of my early "C" or "BASIC" times where there was an "if( result < 0) statement after every function call.

heinzk
Автор

Am I the only one that finds the exception based approach easier to read and understand what is happening? The idea that “anything at any point can throw an exception” seems desirable to me, for known business exceptions like “ValidationException”. Once the call stack gets pretty deep, with nested object calls, it seems like you’ll have a lot of code needing to check the Result.Success to determine if it should move forward.

Wondering what people think about that given perf not being that important.

BrendonParker
Автор

This is basically the same age old discussion of implicit return via exceptions or explicit checks for error statuses. C++ went the way of exceptions, Go went the way of checking for return values. I can see people preferring one over the other. I prefer exceptions because you are working with a language / platform which favors exception so integrating with 3rd party libraries is easier because exception is commonly used (unlike Result / Maybe / Optional / whatever functional flavor you like). In addition to this you will probably always have to take care of exceptions so you might as well use them as well.
As for performance I would consider this a a kind of micro-ptimization which is not really needed with an example application that was provided.

Автор

One complaint (of several) that I have about this technique is that you still need to handle thrown exceptions. Your code - and 3rd party libraries - could still throw exceptions. Therefore you have to implement two different failure handling techniques. In Nick's example, you would still need to (e.g.) put a try/catch around the .Match() code.

Rick-mfgh
Автор

Result<T> might be useful for a limited number of use cases such as validation, but advocating it as a replacement for exceptions in general is a case of "those who fail to understand exceptions are condemned to reinvent them, badly."

There are two very good reasons why exceptions were invented in the first place. First, they convey a lot of important information, such as diagnostic stack traces, and secondly, in 99% of cases, they do the safe and correct thing by default.

Error conditions, whether reported by means of an exception or Result<T>, indicate (or at least they should indicate) that the method you called was not able to do what its name says that it does. In such a situation, it is almost never appropriate to just carry on regardless: if you did, your code would be running under assumptions that are incorrect, resulting in more errors at best and data corruption at worst.

Yet Result<T>, as with return codes that predated exception handling, makes this incorrect and potentially dangerous behaviour the default. This means that every single call to a method that returns Result<T> needs to be followed by a whole lot of repetitive boilerplate code. And you need to do this right the way through your entire codebase, not just in your controllers.

Most of the time, what you will be doing in response to a failed Result<T> is simply returning another failed Result<T> up to your method's caller. But this is exactly what exceptions give you out of the box anyway. The whole point of exceptions is to take this repetitive boilerplate code and make it implicit. In the minority of cases where that isn't the appropriate behaviour, try/catch/finally blocks give you a way to override it to do things such as cleaning up or attempting to recover from the situation.

Now to be fair, there are a couple of possible use cases for Result<T>. It might be useful if you have one specific error condition that you expect to be encountering frequently, such as invalid input or requests for nonexistent resources, and that you need to handle there and then in a specific way. So it's probably fine for such things as validation. But it should most certainly not be used as a replacement for exceptions in general.

jammycakes
Автор

Funny, we do exactly this and are seriously considering just going back to exceptions and a custom filter. I'm really starting to think the trade off isn't worth it. 99% of the time I just want to shortcircuit the rest the logic and return an error, and that's exactly what an exception is designed to do.

sleeper
Автор

Outside of what already have been mentioned this approach generates a ton of boilerplate code in a bigger application. I don't think its a way to go, I personally prefer exceptions.

whatisgosu
Автор

Most of the times I've seen this pattern, it had the unfortunate side effect of hiding errors and exceptions because calling code almost never does anything useful with the Result object if the result is not Success. When a user clicks a button and the process behind that button goes wrong, I'd rather have an exception than simply having nothing happen, and you have to check all your backends to see whether the button click actually occurred. The calling code can only work with a Result object if it knows what to do with it, and any error message in the Result object is probably not suitable to show to the user anyway.

notpingu
Автор

I'm using the first approach of throwing exceptions and catching them in a middleware, yea.

The issue with the second approach is that every method needs to have code for handling invalid results. The exception could come from like 5 layers deep... having to handle exceptions in every layer and step back/ return naturally is just annoying.
I know it's not really a popular opinion, but to me code seems a lot cleaner when it mostly only has to handle the happy-flow. And using exceptions to basically do a longjmp to a middleware is just easier.

Plus I'm assuming that 90%+ of the calls are going to follow the happyflow. So from a "exception only" benchmark it seems like you can handle errors 3 times faster, great. But my applications are not aimed towards users doing everything wrong and being able to tell them that _slightly faster_

ronsijm
Автор

As someone who had to confront C's _setjmp_ / _longjmp_ terror during the 80's and 90's (with the compiler differences [and the platform-dependent implementations of things _called_ exceptions] they could expose), my point of view has not changed in C#: if error conditions are known/predictable/constrained, use/check return values and handle them sensibly and readably, as locally as possible; if error conditions are *_exceptional, _* use exceptions.

EduardQualls
Автор

I love this kind of approach ... as long as language supports it. It works nicely in Rust and Haskell that both have discriminated unions, 'match' is a keyword and have some operators to reduce boilerplate (>>= in Haskell or ? in Rust). If language forces you to handle all error cases properly then it results with much more correct code.

In C# however, code becomes bloated quite quick. If you use `async/await` it becomes even more clumsy as it hinders fluent API chains that involve it. Or if one arm of match calls an async function, it affects the type of other arms (e.g need to wrap in Task.FromResult).

Also, video completely ommits the cost of happy path. After all in happy path, a struct is passed with reference to a an object or an exception and some enum value. It should be allocated on stack ... but with async it will be boxed, and unboxed possibly a few times. Allocations have its cost. This kind of approach reduces cost of error path, but makes happy path more slightly costly. There is a point where this is performance improvement to use Result instead of Exceptions, but this needs to be benchmarked, not assumed. Not to mention that for most applications performance is not a primary concern.

Ailurophopia
Автор

I much rather prefer explicit error handling by return value than dealing with an exception you probably didn’t know about until you hit it.

DynamicalisBlue
Автор

Really loving the LanguageExt library. I started using it for the Option<T> but I'm always learning new stuff by using it. Seems like Result<T> will be clearer to use rather than Either<Error, T>. Thanks for this video!

Funestelame
Автор

If performance is crucial, then this is the ideal approach. It's very rare for this to be a bottleneck though. How many people in the comments are dealing with a web app needing to handle 1000s of validation errors a second? Does this approach make a difference with client-side validation and happy path?

Even with this approach, you still need a strategy for handling exceptions for "expected" scenarios, for example database constraint violations and concurrency errors. These types of errors should arguably not return 500 status, but 409, so you end up needing to implement your exception filters anyway! So ultimately this adds significant overhead for small teams.

As is mentioned elsewhere, this pattern will pollute all your service layers and require you to check for errors all the way up the chain. I think what's telling is how few popular libraries implement this pattern. It's not so bad if you have a pretty flat CQRS architecture I suppose.

Full disclaimer: you're still my favourite Nick xox.

evilpigeon
Автор

The main problem with exceptions is that they are very expressly named that and everybody keeps ignoring that.

Exceptions are supposed to be exceptional. They are for when your code does weird stuff you didn't expect or guard against and you actually can't recover to a defined state on your level of execution. At that point the exception basically becomes a hail mary you throw up the stack in the hopes that someone higher up the food chain actually has enough knowledge about the application state to recover the application to a defined state. (or at least ad additional logs about what exactly went wrong to help you diagnose the failure) .

If your Exceptions happen at a frequency where the performance impact of using them is a concern to you you are already doing it wrong, because then clearly they are not a rare event and you're using Exceptions for control flow at that point which application code never should do.
The TPL and async await do use exceptions for control-flow (e.g. TaskCanceledException) because they literally have no better way to achieve this feature. But e.g. a call to a server timing out and throwing a TimeoutException is just something you have to expect and therefore handle gracefully. And not by rethrowing the exception you got from the framework up the chain.

dgschrei
Автор

This video is so exceptional, it really changed the way I work with exceptions

redslayer
Автор

Personally, I like Exceptions in human time and return value models like this for computer time. Exceptions are great for handling error conditions because you can include the data necessary for calling functions to adapt, retry, or worst case build a chain of messages for support to debug and users to know something went bonk. But a server system doesn't need clever messaging and can easily act on return codes which are MUCH faster than exceptions. The other beauty of exceptions is the immediate halt of execution because you can write your code as though everything is hunky-dory and know that if x is 0 you're not introducing NaN into the data stream when you execute 3 / x. That's a stupidly simple example, but you see the point. And for clarity, they're not really "go tos" they're more like interrupts. In fact, IIRC, in C or C++ they were implemented with a specific interrupt codes at the OS level so that it would halt program execution and dump to the exception handling logic.

jimread