Why 'Just Use Exceptions' Is Bad Advice in .NET
45sDirectly challenges a common programming belief, sparking debate and curiosity.
▶ Play Clip"The title is a direct and accurate summary of the video's argument, delivering exactly what it promises."
The video discusses the controversial topic of using the result pattern instead of exceptions for flow control in .NET applications. The presenter refutes common criticisms and demonstrates a practical refactoring example, showing how results can make code more explicit and maintainable.
A simple result class with success/failure flag, error code/description, and helper methods went viral with over 150k views.
Using exceptions for flow control is an anti-pattern; exceptions act like goto statements, jumping to global handlers.
You can capture a stack trace using Environment.StackTrace without throwing an exception.
Refactoring involves replacing expected exceptions with failure results, and defining a static class to document possible errors.
The result pattern makes APIs more honest by explicitly stating that a method can fail, forcing callers to handle failures.
Some exceptions should still be thrown, such as unrecoverable scenarios like a missing API key.
C# lacks native discriminated unions, but you can implement helper methods like 'match' to simplify result handling.
Use exceptions for broken application states, and results for expected business failures that clients can handle.
Why is using exceptions for flow control considered an anti-pattern?
Using exceptions for flow control is an anti-pattern because exceptions are meant for exceptional, unexpected situations, not predictable business failures.
02:55
What does a basic result class typically contain?
The result pattern wraps a success or failure flag, an error with a code and description, and helper methods to create success or failure results.
00:17
How can you get a stack trace without using an exception?
You can use the Environment.StackTrace property to capture a stack trace without throwing an exception.
04:19
When should you use an exception instead of a result?
Exceptions should be used for unrecoverable scenarios like a missing API key, while results are for expected failures the client can handle.
11:53
What language feature does C# lack that F#, Go, and Rust have?
C# does not natively support discriminated unions, unlike F#, Go, and Rust.
08:44
Exceptions as Goto
Explains why exceptions for flow control are an anti-pattern, comparing them to goto statements.
02:55Stack Trace Without Exceptions
Shows how to capture a stack trace using Environment.StackTrace, a lesser-known technique.
04:19Honest API Design
Highlights how the result pattern makes APIs more honest by explicitly stating possible failures.
06:07When to Use Exceptions
Provides clear guidance on when exceptions are appropriate versus when to use results.
11:53[00:02] like don't use exceptions for flow control are so controversial in the .NET space? I'm talking about using the result pattern to get rid of unnecessary exceptions in your code and my recent post on social media that went pretty
[00:17] viral. Here's my recent post on X that went fairly viral with over 150k views and all I did here was just showcase a simplified implementation of a result class. It contains a success or failure flag. It wraps an error which consists
[00:32] of a code and a description letting you represent different kinds of errors in your applications. And then there's a helper method to create a success or a failure result. Obviously, you would also have a result of T in your code
[00:44] base to also wrap some sort of value where this simple result would be a result of T where T is void. And this little post garnered a lot of attention ranging from why are you even doing this? Just use exceptions. You write
[00:57] your code to F sharp, go, or rust because they support this and all kinds of nonsense even calling this a terrible idea. And I just wanted to make this video to refute some of these claims and show you a practical example of why
[01:10] using a result might be valuable and then you'll be the judge of if you want to use this in your applications or not. Here's an example of an application service in a typical enterprise .NET application. It orchestrates a couple of
[01:23] dependent services, usually some sort of repositories, maybe some payment of the implementation is that we are using some specific exceptions to implement an early return mechanism where we run into a situation in this
[01:36] method's execution that we can't recover from. So for example, if the concert specified by the concert ID doesn't exist, then we're going to throw a concert not found exception. If there are no more available tickets, we're
[01:49] going to throw an insufficient seats exception. If the user trying to book a a user not found exception. And you get the idea. We keep throwing some custom that we can't recover from. And in the end we get to the happy path which is
[02:03] calling our payment gateway to charge the user, creating a ticket, and then storing that inside of our database. Now what's interesting with this approach and how this is usually implemented in most applications is that there's no
[02:16] global try catch within this method itself handling any really unexpected failures. For example, the payment gateway throwing anything else other than the payment declined exception, our call to the database could also fail for
[02:30] various reasons. We're not handling any of those in this method. And what you usually have in the top level is some sort of global exception handler or a middleware that's just going to wrap this in a problem details, return it to
[02:42] the client with more or less information about what went wrong on the back end, and then the client application is supposed to figure out what to do based on the response from the server. And this is exactly the anti-pattern that I
[02:55] try to avoid when I'm building applications and it's called using exceptions for flow control. What you end up building is a go to statement which is basically what an exception is. It's a jump command at the assembly
[03:08] somewhere else. In the majority of cases, this go to is going to jump into some global handler that's not doing anything special other than converting this into a problem details response,
[03:20] can examine later when viewing your structured logs. And what's really exceptions is that they are usually thrown for the exact opposite reason of exception represents something exceptional in your code base where you
[03:35] can't continue with the normal flow of execution. A concert being null in your database isn't all too exceptional mainly because you can predict it. If you are aware of why this condition might fail, then this isn't really an
[03:49] exceptions for flow control where I think the result pattern is a much implementation, again simplified. All I have is a result of T, so I'm wrapping and I have helper methods for creating a success or failure result. The error can
[04:05] be anything you like. It can be an enum, in my case it's a record with a code and a description and a stack trace. Yes, that's right. You don't need to use an exception to get a stack trace. You can create a stack trace anywhere you want.
[04:19] There's an environment property called a stack trace which you can store when instantiating an error, grab a stack trace where this error was created. So you're not only limited to exceptions to be able to figure out where some sort of
[04:32] failure occurred. You could also log the stack trace in your structured logs and be able to debug where exactly the failure happened inside of your code refactor because we aren't really changing the behavior of the ticket
[04:45] booking service to use a result approach, we would start by updating the throwing exceptions, we can use a failure result to represent unexpected scenario. So I would say result ticket failure and then I've got a set of
[05:01] ticket errors and in this case it's going to be concert not found. So another benefit of this is you can define a static class like in the example here which is going to document the possible failures inside of our
[05:13] application. So by looking at this, I can figure out that the possible errors are the concert is not found, a concert is not finished, insufficient seats, a error. You can just easily publish this in your CI pipeline into documentation
[05:27] on your website. So continuing with our refactor, we replace the expected exceptions with a failure type. So instead of throwing an insufficient seats exception, I can return a respective error in a failure result.
[05:40] Same goes for the user not found exception. I can return a failure result that contains the user not found error by saying result ticket failure and then I've got ticket errors user not found. The next question I often see is does
[05:54] using a result pattern mean you can't throw exceptions at all? And the answer is absolutely not. This doesn't eliminate all exceptions in your code base, just the expected ones. It also makes your public API more honest.
[06:07] Previously, this was returning a task of ticket implying that you would always true. In some cases, this method could throw and terminate the execution where the execution would jump to a completely unrelated part of the code base to
[06:20] handle this exception and return some sort of response. With a result of ticket, we are being honest that this can return a result containing a ticket or it could also possibly fail meaning that it's your responsibility as the
[06:33] caller of this API to handle a failure scenario. And I think you'll agree that code like this is much easier to reason about rather than if it were using an exception. Now an exception to this and there are exception to all kinds of
[06:45] rules even when using a result pattern is that some exceptions should simply be thrown. So a typical example of this is some sort of unrecoverable scenario like an API key missing from your payment settings. Forgive me for not being more
[06:59] came up with. So in this case it's perfectly valid to throw an exception like an invalid configuration exception that will bubble up somewhere and be handled by a global exception handler, that's perfectly fine. You don't have to
[07:13] where it's not meant to be used. And when you think about it, this really is an exceptional scenario. In all normal execution, you would expect the API key to be present inside of some environment variable or application secret. And if
[07:27] exceptional and you should throw an exception. Moving on, I'm going to continue removing the expected exceptions and replace them with a failure result. So I'll say result
[07:39] failure result. So I'll say result ticket failure and then ticket errors. And here we would return payment declined as the respective error. And finally, you have to return some sort of result. So in this case it's going to be
[07:52] a success result wrapping our object. Now you could also implement an implicit conversion from an object into a success result. I don't think that's necessary here, so I'm going to leave it at that. Now what happens to the code that was
[08:04] calling our API? I've got a ticket controller here that was using the previous implementation that would be throwing exceptions. And here you could implement something like this, a try block and multiple catch blocks for the
[08:16] could see. And then based on the type of exception, you could explicitly handle it. Or if it wasn't handled here, then hopefully there's a global catch all handler that's going to handle any missed exceptions. With our new API, we
[08:29] return a result object that explicitly states that it can succeed or fail and then you can decide how to act on the failure. The problem that we currently some time in the future, it may never be solved, and that is that C# doesn't
[08:44] natively support discriminated unions. Languages like F#, Go, and Rust have this built in, but C# as of now does not. So you're going to hear this argument quite often, why don't you just use Go or Rust or F#? And the answer is
[08:57] I simply don't want to. I want to continue using C# and .NET and all this ecosystem has to offer. Also the idea of joining the 10 other F# developers out me. A quick example of what you can also implement on the result object is having
[09:12] a method like this. I usually call it match and this lets you have a callback where you can access the value of the result and return some sort of T out. Of course, I have to make this generic and this would be our on success callback.
[09:27] And I also have another callback exposing an error and let's say returning the same value and this would be on failure. And the implementation is very simple. If this is a success result, then we just call on success and
[09:41] pass in the value. Otherwise, we call on failure and pass in the error. Of course, it's implied that the value would be not null in the success scenario. You can also check for this in our implementation. What this allows me
[09:54] our implementation. What this allows me to do is say return result match and then my success callback is going to take the value and return it by calling to take the error and then I could have this logic here that checks what the
[10:09] contents of the error are. So, I'm going to simplify this code a bit to depend on my local error variable and we would have something like this. We could replace the first example with just a method group because it exactly matches
[10:22] the contract of the okay method. And another thing that's useful is having a switch expression on the error or more specifically the error code where a syntax like mapping this with a static value like this would be really helpful
[10:35] to return the expected failure. Let's see if I can make this into a switch expression and I'm pretty sure that this isn't valid C# code because it requires a constant value whereas this is a static value. But nonetheless, a syntax
[10:47] like this that allows you to exhaustively map over the possible failure results would be very helpful to make this a more useful API surface. Another pushback I hear is having to check if this is a success or failure
[11:00] result like in this example here. And my answer to this is this is kind of the point when you think about it. I'm being explicit that the method I'm calling can fail and also want to handle this failure and see what I can do about it.
[11:12] Another pushback is having to write if statements in your entire code base to check if a result is a success or failure one and you don't necessarily helper methods like the match method here. And also, you don't really want to
[11:26] overuse the result pattern everywhere because most of the time it's not the happy path. If there are any exceptions, you can also handle those explicitly and return a value without having to rely on a result. And I'm sure
[11:39] that people have mentioned. I just don't want to go on rambling about this for too long. Like here's my overall conclusion on when you should use a exception. An exception is the most appropriate solution for when your
[11:53] application is broken and there's nothing you can do about it. So, the exception and terminating the execution. A result is useful when this is a failure that the client can handle or
[12:05] something that you can expect and it's a valid part of your business logic like a concert or user being null or having an insufficient number of seats available to book a ticket. You're also forcing the caller to handle this explicitly and
[12:19] have than depending on exceptions because they are implicit and there are no good ways in C# in my opinion to document that they exist. Yes, C# allows you to document your exceptions using
[12:32] something like this where if you now hover over this method, you would see this isn't too terrible. However, I still prefer using a result to express my possible failure conditions. Also, if
[12:44] you want to see why using an exception is expensive and you care about performance, then I recommend taking a look at this video next. Consider leaving a like on this video if you enjoyed it. Thanks a lot for watching
[12:56] enjoyed it. Thanks a lot for watching and until next time, stay awesome.
⚡ Saved you 0h 12m reading this? Transcribe any YouTube video for free — no signup needed.