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.
[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.
Which is exactly what IMO the author should have done. It's actually a reasonable use-case for inheritance:
#nullable enable
record SlackEvent
( int EventId
, string Content
, string? TeamId
);
record TeamSlackEvent
( int EventId
, string Content
, string TeamId
)
: SlackEvent
( EventId : EventId
, Content : Content
, TeamId : TeamId
);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.
There was an UnreachableException before .NET 7 as well.
Look:
public class UnreachableException : Exception {}
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
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.
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 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.
Now, for a better name? Maybe ShouldNeverGetHereException, but that may be seen as too whimsical.
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.
Rust actually has two things called "unreachable". There's the unreachable!() macro (https://doc.rust-lang.org/core/macro.unreachable.html), which is a short-hand for panic!(), and therefore is never UB even if somehow it's reached; and there's the unsafe unreachable_unchecked() compiler hint (https://doc.rust-lang.org/core/hint/fn.unreachable_unchecked...), which is optimized away in release code, and is always UB if somehow it's reached. Most of the time, you should prefer the safer unreachable()! macro; the unsafe hint is for when it makes a performance difference (and as with every use of unsafe in Rust, you really should carefully review the code to make sure it's really unreachable).
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...
[0] https://learn.microsoft.com/en-us/dotnet/api/system.invalido...
This is exactly the issue what Rust is solving. A struct construction completes or fails. There is no weird in between state.
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.
If TeamId goes from non-nullable to nullable, it goes from TeamId to Option<TeamId>, and consumers get a type error.
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.
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.
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.
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...
By example, for a switch on an enum, the compiler inserts a "throw new ...Error()" automatically when the default case is not specified.