but crashing your client's software is still not a good thing
It most certainly is a good thing.
You want anything that leaves the system in an undefined state to stop the system because an undefined system can do nasty things like corrupt data, format the hard drive, and send the president threatening emails. If you cannot recover and put the system back into a defined state then crashing is the responsible thing to do. It's exactly why we build systems that crash rather than quietly tear themselves apart. Now sure, we all want a stable system that never crashes but we only really want that when the system stays in a defined predictable safe state.
I've heard that exceptions as control flow are considered a serious antipattern
That's absolutely true but it's often misunderstood. When they invented the exception system they were afraid they were breaking structured programming. Structured programming is why we have for
, while
, until
, break
, and continue
when all we need, to do all of that, is goto
.
Dijkstra taught us that using goto informally (that is, jumping around wherever you like) makes reading code a nightmare. When they gave us the exception system they were afraid they were reinventing goto. So they told us not to "use it for flow control" hoping we'd understand. Unfortunately, many of us didn't.
Strangely, we don't often abuse exceptions to create spaghetti code as we used to with goto. The advice itself seems to have caused more trouble.
Fundamentally exceptions are about rejecting an assumption. When you ask that a file be saved you assume that the file can and will be saved. The exception you get when it can't might be about the name being illegal, the HD being full, or because a rat has gnawed through your data cable. You can handle all those errors differently, you can handle them all the same way, or you can let them halt the system. There is a happy path in your code where your assumptions must hold true. One way or another exceptions take you off that happy path. Strictly speaking, yeah that's a kind of "flow control" but that's not what they were warning you about. They were talking about nonsense like this:
"Exceptions should be exceptional". This little tautology was born because the exception system designers need time to build stack traces. Compared to jumping around, this is slow. It eats CPU time. But if you're about to log and halt the system or at least halt the current time intensive processing before starting the next one then you have some time to kill. If people start using exceptions "for flow control" those assumptions about time all go out the window. So "Exceptions should be exceptional" was really given to us as a performance consideration.
Far more important than that is not confusing us. How long did it take you to spot the infinite loop in the code above?
DO NOT return error codes.
...is fine advice when you're in a code base that doesn't typically use error codes. Why? Because no one's going to remember to save the return value and check your error codes. It's still a fine convention when you're in C.
OneOf
You're using yet another convention. That's fine so long as you're setting the convention and not simply fighting another one. It's confusing to have two error conventions in the same code base. If somehow you've gotten rid of all code that uses the other convention then go ahead.
I like the convention myself. One of the best explanations of it I found here*:
But much as I like it I'm still not going to mix it with the other conventions. Pick one and stick with it.1
1 : By which I mean don't make me think about more than one convention at the same time.
Subsequent thoughts:
From this discussion what I'm taking away currently is as follows:
- If you expect the immediate caller to catch and handle the exception most of the time and continue its work, perhaps through another path, it probably should be part of the return type. Optional or OneOf can be useful here.
- If you expect the immediate caller to not catch the exception most of the time, throw an exception, to save the silliness of manually passing it up the stack.
- If you're not sure what the immediate caller is going to do, maybe provide both, like Parse and TryParse.
It's really not this simple. One of the fundamental things you need to understand is what a zero is.
How many days are left in May? 0 (because it's not May. It's June already).
Exceptions are a way to reject an assumption but they are not the only way. If you use exceptions to reject the assumption you leave the happy path. But if you chose values to send down the happy path that signal that things are not as simple as was assumed then you can stay on that path so long as it can deal with those values. Sometimes 0 is already used to mean something so you have to find another value to map your assumption rejecting idea on to. You may recognize this idea from its use in good old algebra. Monads can help with that but it doesn't always have to be a monad.
For example2:
IList<int> ParseAllTheInts(String s) { ... }
Can you think of any good reason this must be designed so that it deliberately throws anything ever? Guess what you get when no int can be parsed? I don't even need to tell you.
That's a sign of a good name. Sorry but TryParse is not my idea of a good name.
We often avoid throwing an exception on getting nothing when the answer could be more than one thing at the same time but for some reason if the answer is either one thing or nothing we get obsessed with insisting that it give us one thing or throw:
IList<Point> Intersection(Line a, Line b) { ... }
Do parallel lines really need to cause an exception here? Is it really that bad if this list will never contain more than one point?
Maybe semantically you just can't take that. If so, it's a pity. But Maybe Monads, that don't have an arbitrary size like List
does, will make you feel better about it.
Maybe<Point> Intersection(Line a, Line b) { ... }
The Monads are little special purpose collections that are meant to be used in specific ways that avoid needing to test them. We're supposed to find ways of dealing with them regardless of what they contain. That way the happy path stays simple. If you crack open and test every Monad you touch you're using them wrong.
I know, it's weird. But it's a new tool (well, to us). So give it some time. Hammers make more sense when you stop using them on screws.
If you'll indulge me, I'd like to address this comment:
How come none of the answers clarifies that the Either monad is not an error code, and nor is OneOf? They are fundamentally different, and the question consequently seems to be based on a misunderstanding. (Though in a modified form it’s still a valid question.) – Konrad Rudolph Jun 4 `18 at 14:08
This is absolutely true. Monads are much closer to collections than exceptions, flags, or error codes. They do make fine containers for such things when used wisely.
Result
;-)FileNotFoundException
.Either
monad is not an error code, and nor isOneOf
? They are fundamentally different, and the question consequently seems to be based on a misunderstanding. (Though in a modified form it’s still a valid question.)