The state machine example is a good one. It's a textbook case of unreachable code.
The null-checking example is not good. If the external API returns a null where you don't expect one, UnreachableException will be thrown. It doesn't matter if the documentation says it will never be null; clearly the code isn't unreachable. All you need to reach it is bad data! (InvalidDataException would be a better choice, though arguably not an ideal one.)
If you can write a unit test that makes your code throw UnreachableException, you almost certainly should be throwing a different exception.
This was my initial thought as well, but from the text I gather there is a flow like this:
[Input Data, maybe null] -> Validate field is not null -> Call this method with the assertion.
This is a small bug-bear for me with nullable types and I wish there was a better way to do it, but many languages allow you to smart-cast away nulls, but only within the local scope. If you want to pass a struct-type around which has nullable fields, but you have already checked for non-null (like this one) you need to convert to a different struct-type, which doesn't have the nullability on its fields. I can't think of a good way round this - as you say with the unit test remark, there is nothing to stop another piece of code calling this method with nulls.
Underrated comment. I’m all for precisely modeling domain and I/O errors. But for “this is a bug” type of exceptions, what good do all those tiny variations (and the resulting philosophical debates about when to use which one) do, if all that changes in practice is the text of log message? You’ll never catch that one specifically or treat it any differently than an ArgumentNullException — or any other of the zillion already existing ones.
Just wanted to highlight a relevant tool i recently discovered: Sentry.
It's already become invaluable in my .net apps for tracking down tricky errors, especially in the new MAUI apps.
Caught one just yesterday where the app was trying to call an api with decimals serialized with a comma because it was on a dutch user's phone! I imagine it would've been quite the headache catching that without sentry. Integrates with most tech-stacks too, not just .net btw
Had to google Sentry, I've been using Raygun for ~10 years. These sort of tools are awesome. The weird errors that users seem to be able to produce but getting a full stack trace and being able to resolve them.
My previous job we used to send exceptions to email until we blew out the gmail account for the day (exceeded the receive amount) and boss switched over to raygun (~10 years ago), because of the global error handling we ended up fixing tons of errors we didn't even know our customers were experiencing. Product became more stable over time with the fixes.
> with decimals serialized with a comma because it was on a dutch user's phone
If you're doing any kind of non-binary serialization/deserialization or file IO, it's always a good exercise to change the user and system regional settings when running tests.
For null checking I'd use the relatively new ArgumentNullException.ThrowIfNull, that automatically adds the parameter name in the exception and is a very compact and obvious way to check for null in your method parameters.
For enums I use ArgumentExceptions, something called this code with an invalid enum argument. I think there are some cases where Unreachable is a better error, but I'd probably be more inclined to use that for logic errors, where the code itself should not be reachable according to your understanding and reaching it would mean the code is wrong. The enums and null are cases where the arguments are unexpected, which to me is different.
I don’t think that’s a good name for this. Clearly, that code can be reached. From the name, I would expect ”throw new UnreachableException” to be something the compiler inserts to make it more robust in the sense that, if the compiler has some bug in its control flow analysis, it throws, rather than executes the wrong code.
Now, for a better name? Maybe ShouldNeverGetHereException, but that may be seen as too whimsical.
It's anagolous to `unreachable` in Rust. I think the author is defining it too broadly. It's for scenarios where it would be possible to statically guarantee that something is unreachable, but is not implemented in control flow analysis (e.g. it could be too computationally expensive, or it could be a structure invariant). It's useful for getting the compiler to shut up about a branch that you know can't be reached, e.g. you might not return a value from the `default` case in an exhaustive switch.
It doesn't seem like .Net is implementing it correctly either. It is supposed to be optimized away in release code, resulting in UB if the developer was wrong about the condition.
I agree, `UnreachableException` is really poorly used here, and most of these are argument exceptions, and clearly are reachable. However, for other "unexpected behaviors", you should really throw an `InvalidOperationException` - you did something that is invalid. And even better if you sub-class Invalid with a custom exception that more clearly describes the unexpected behavior (also easier to search for that exception type in your log analysis tool).
Ignoring the subject matter entirely, it's always fun to see some snippets of C# du jour and marvel at the number of new syntactical features added since my last encounter.
Yeah, it started like C++ for wannabee programmers, but during the last years it has being becoming a more and more useful tool for professional developers.
seems to me they are using unreachable to mean unsupported. Also they should put a message in for the first example which should highlight the int value as that is the very first thing you are going to want to know.
That’s all well and good when you write both the consumer and producer. But how do you detect consumers which depend on some data integrity aspect you need to change for whatever reason?
I think I still prefer NotImplementedException. It also covers things that are logically reachable but not ready for use yet. The nuance is the string argument to the ctor.
It's not like you can't define your own exceptions in C#, so you always could do what the article is saying.
Even if exceptions might be handy, I wouldn't use exceptions for error handling for permance reasons. Instead I have another recommendation: never have void methods and use a Result type that packages the actual value along with an IsValid and Error property. So you will always return Result<T>.
That way, error handling is easy and doesn't cause much performance overhead.
I don't think you'd be hitting UnreachableException that much for that to be of performance concern. You actually halt the execution.
And what you are proposing, it's just a different way to handle errors. Maybe appropriate in critical path. However you still have exceptions from .NET framework. OutOfMemoryException, various IOExceptions etc.
UnreachableException has no special behaviour at all [0]. It's just a standardized 'tag' around regular Exception.
The only advantage of using a standard tag is that it will be recognized by other programmers, and maybe some supporting tooling might choose to give it special treatment.
E.g. a code analyzer could warn you if you raise a NotImplementedException (forgot to finish a feature?), but accept an UnreachableException as valid.
If you want to add a custom name (for easier grepping maybe?) or stuff some extra data in the exception, you can still inherit from UnreachableException.
[+] [-] Mindless2112|3 years ago|reply
The null-checking example is not good. If the external API returns a null where you don't expect one, UnreachableException will be thrown. It doesn't matter if the documentation says it will never be null; clearly the code isn't unreachable. All you need to reach it is bad data! (InvalidDataException would be a better choice, though arguably not an ideal one.)
If you can write a unit test that makes your code throw UnreachableException, you almost certainly should be throwing a different exception.
[+] [-] ad133|3 years ago|reply
[Input Data, maybe null] -> Validate field is not null -> Call this method with the assertion.
This is a small bug-bear for me with nullable types and I wish there was a better way to do it, but many languages allow you to smart-cast away nulls, but only within the local scope. If you want to pass a struct-type around which has nullable fields, but you have already checked for non-null (like this one) you need to convert to a different struct-type, which doesn't have the nullability on its fields. I can't think of a good way round this - as you say with the unit test remark, there is nothing to stop another piece of code calling this method with nulls.
[+] [-] gwbas1c|3 years ago|reply
ArgumentNullException, InvalidOperationException... There's plenty that have been around since the beginning.
What's more important, that I didn't see in the first two examples, are useful error messages.
[+] [-] WirelessGigabit|3 years ago|reply
[+] [-] dustedcodes|3 years ago|reply
There was an UnreachableException before .NET 7 as well.
Look:
public class UnreachableException : Exception {}
[+] [-] codeflo|3 years ago|reply
[+] [-] insaider|3 years ago|reply
It's already become invaluable in my .net apps for tracking down tricky errors, especially in the new MAUI apps.
Caught one just yesterday where the app was trying to call an api with decimals serialized with a comma because it was on a dutch user's phone! I imagine it would've been quite the headache catching that without sentry. Integrates with most tech-stacks too, not just .net btw
[+] [-] philliphaydon|3 years ago|reply
My previous job we used to send exceptions to email until we blew out the gmail account for the day (exceeded the receive amount) and boss switched over to raygun (~10 years ago), because of the global error handling we ended up fixing tons of errors we didn't even know our customers were experiencing. Product became more stable over time with the fixes.
[+] [-] magicalhippo|3 years ago|reply
If you're doing any kind of non-binary serialization/deserialization or file IO, it's always a good exercise to change the user and system regional settings when running tests.
[+] [-] solarkraft|3 years ago|reply
[+] [-] fabian2k|3 years ago|reply
For enums I use ArgumentExceptions, something called this code with an invalid enum argument. I think there are some cases where Unreachable is a better error, but I'd probably be more inclined to use that for logic errors, where the code itself should not be reachable according to your understanding and reaching it would mean the code is wrong. The enums and null are cases where the arguments are unexpected, which to me is different.
[+] [-] Someone|3 years ago|reply
Now, for a better name? Maybe ShouldNeverGetHereException, but that may be seen as too whimsical.
[+] [-] zamalek|3 years ago|reply
It doesn't seem like .Net is implementing it correctly either. It is supposed to be optimized away in release code, resulting in UB if the developer was wrong about the condition.
[+] [-] magicalhippo|3 years ago|reply
In Delphi we have the EProgrammerNotFound exception[1]. I use it for code paths which should never be executed.
[1]: https://docwiki.embarcadero.com/Libraries/Alexandria/en/Syst...
[+] [-] whoisthemachine|3 years ago|reply
[0] https://learn.microsoft.com/en-us/dotnet/api/system.invalido...
[+] [-] rbanffy|3 years ago|reply
[+] [-] afiori|3 years ago|reply
[+] [-] thom|3 years ago|reply
[+] [-] pif|3 years ago|reply
[+] [-] keithnz|3 years ago|reply
[+] [-] WirelessGigabit|3 years ago|reply
This is exactly the issue what Rust is solving. A struct construction completes or fails. There is no weird in between state.
[+] [-] piaste|3 years ago|reply
The issue they had is:
(a) TeamId is legitimately nullable at creation, there are valid SlackEvents without a TeamId
(b) They wanted to assert "TeamId won't be null" from a certain point in the code path
(c) They didn't want to create a different data structure (TeamSlackEvent or something) to hold the definitely-has-a-TeamId events.
I strongly suspect (c) was a mistake and they were just too lazy to define a new (sub)-class or record.
[+] [-] MobiusHorizons|3 years ago|reply
[+] [-] bob1029|3 years ago|reply
[+] [-] Deukhoofd|3 years ago|reply
[+] [-] hardware2win|3 years ago|reply
[+] [-] kevingadd|3 years ago|reply
[+] [-] DeathArrow|3 years ago|reply
Even if exceptions might be handy, I wouldn't use exceptions for error handling for permance reasons. Instead I have another recommendation: never have void methods and use a Result type that packages the actual value along with an IsValid and Error property. So you will always return Result<T>.
That way, error handling is easy and doesn't cause much performance overhead.
[+] [-] branko_d|3 years ago|reply
Exceptions are slow when thrown.
If an error happens rarely, the corresponding exception is thrown rarely, so the performance impact is minimal.
OTOH, if an error can happen frequently, it is no longer "exceptional", and so is probably better handled without exceptions anyway.
[+] [-] jve|3 years ago|reply
And what you are proposing, it's just a different way to handle errors. Maybe appropriate in critical path. However you still have exceptions from .NET framework. OutOfMemoryException, various IOExceptions etc.
[+] [-] hardware2win|3 years ago|reply
[+] [-] Deukhoofd|3 years ago|reply
[+] [-] VoidWhisperer|3 years ago|reply
[+] [-] oaiey|3 years ago|reply
[+] [-] kagaw|3 years ago|reply
[+] [-] piaste|3 years ago|reply
The only advantage of using a standard tag is that it will be recognized by other programmers, and maybe some supporting tooling might choose to give it special treatment.
E.g. a code analyzer could warn you if you raise a NotImplementedException (forgot to finish a feature?), but accept an UnreachableException as valid.
If you want to add a custom name (for easier grepping maybe?) or stuff some extra data in the exception, you can still inherit from UnreachableException.
[0] https://github.com/dotnet/runtime/pull/63922/files#diff-588c...
[+] [-] mattmanser|3 years ago|reply
[+] [-] nomercy400|3 years ago|reply
[+] [-] orthoxerox|3 years ago|reply
[+] [-] _old_dude_|3 years ago|reply
By example, for a switch on an enum, the compiler inserts a "throw new ...Error()" automatically when the default case is not specified.
[+] [-] 987655321|3 years ago|reply
[deleted]
[+] [-] unknown|3 years ago|reply
[deleted]