To me, this seems safer than exceptions. In C#, ignoring exceptions does not produce a compile error, and if they are not caught they just bubble up and crash your program. This is perhaps better than ignoring an error code and producing undefined behaviour, but crashing your client's software is still not a good thing, particularly when it's performing many other important business tasks in the background.

With OneOf, one has to be quite explicit about unpacking it and handling the return value and the error codes. And if one doesn't know how to handle it at that stage in the call stack, it needs to be put into the return value of the current function, so callers know an error could result.

But this doesn't seem to be the approach Microsoft suggests.

Is using OneOf instead of exceptions for handling "ordinary" exceptions (like File Not Found etc) a reasonable approach or is it terrible practice?

Note I'm not using OneOf for things like "Out Of Memory", conditions I don't expect to recover from will still throw exceptions. But I feel like quite reasonable issues, like user input that doesn't parse are essentially "control flow" and probably shouldn't throw exceptions.

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.

Somewhat off-topic, but note that the approach you're looking at has the general vulnerability that there's no way to ensure the developer handled the error properly. Sure, the typing system can act as a reminder, but you lose the default of blowing up the program for failing to handle an error, which makes it more likely that you end up in some unexpected state. Whether the reminder is worth the loss of that default is an open debate, though. I'm inclined to think it's just better to write all your code assuming an exception will terminate it at some point; then the reminder has less value.
– jpmc26Jun 4 '18 at 9:35

I may be alone on this, but I think that crashing is good. There is no better evidence that there is issue on your software than a crash log with the proper tracing. If you have a team that is able to correct and deploy a patch in 30 minutes or less, letting those ugly buddies show up may not be a bad thing.
– T. SarJun 4 '18 at 12:06

9

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 RudolphJun 4 '18 at 14:08

10 Answers
10

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.

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 RudolphJun 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.

Wouldn't it be more appropriate if the red track was below the boxes, thus not running through them?
– FlaterJun 4 '18 at 9:33

4

Logically, you are correct. However, each box in this particular diagram represents a monadic bind operation. bind includes (perhaps creates!) the red track. I recommend watching the full presentation. It is interesting and Scott is a great speaker.
– GusdorJun 4 '18 at 10:27

6

I think of exceptions more like a COMEFROM instruction rather than a GOTO, since throw doesn't actually know/say where we'll jump to ;)
– WarboJun 4 '18 at 17:10

3

There's nothing wrong with mixing conventions if you can establish boundaries, which is what encapsulation is all about.
– Robert Harvey♦Jun 4 '18 at 20:17

3

The entire first section of this answer reads strongly as though you stopped reading the question after the quoted sentence. The question acknowledges that crashing the program is superior to moving on into undefined behavior: they are contrasting run-time crashes not with continued, undefined execution, but rather with compile-time errors that prevent you from even building the program without considering the potential error and dealing with it (which may end up being a crash if there’s nothing else to be done, but it will be a crash because you want it to be, not because you forgot it).
– KRyanJun 5 '18 at 16:05

C# is not Haskell, and you should follow the expert consensus of the C# community. If you instead try to follow Haskell practices in a C# project, you will alienate everyone else on your team, and eventually you will probably discover the reasons why the C# community does things differently. One big reason is that C# does not conveniently support discriminated unions.

I've heard that exceptions as control flow are considered a serious antipattern,

This is not a universally accepted truth. The choice in every language that supports exceptions is either throw an exception (which the caller is free to not handle) or return some compound value (which the caller MUST handle).

Propagating error conditions upward through the call stack requires a conditional at every level, doubling the cyclomatic complexity of those methods and consequently doubling the number of unit test cases. In typical business applications, many exceptions are beyond recovery, and can be left to propagate to the highest level (e.g. the service entry point of a web application).

@Basilevs It requires support for propagating monads while keeping the code readable, which C# doesn't have. You either get repeated if-return instructions or chains of lambdas.
– Sebastian RedlJun 4 '18 at 7:41

You have come to C# at an interesting time. Until relatively recently, the language sat firmly in the imperative programming space. Using exceptions to communicate errors was definitely the norm. Using return codes suffered from a lack of language support (eg around discriminated unions and pattern matching). Quite reasonably, the official guidance from Microsoft was to avoid return codes and to use exceptions.

There have always been exceptions to this though, in the form of TryXXX methods, which would return a boolean success result and supply a second value result via an out parameter. These are very similar to the "try" patterns in functional programming, save that the result comes via an out parameter, rather than through a Maybe<T> return value.

But things are changing. Functional programming is becoming even more popular and languages like C# are responding. C# 7.0 introduced some basic pattern matching features to the language; C# 8 will introduce much more, including a switch expression, recursive patterns etc. Along with this, there has been a growth in "functional libraries" for C#, such as my own Succinc<T> library, which provides support for discriminated unions too.

These two things combined mean that code like the following is slowly growing in popularity.

It's still fairly niche at the moment, though even in the last two years I've noticed a marked change from general hostility amongst C# developers to such code to a growing interest in using such techniques.

We aren't there yet in saying "DO NOT return error codes." is antiquated and needs retiring. But the march down the road toward that goal is well under way. So take your pick: stick with the old ways of throwing exceptions with gay abandon if that's the way you like to do things; or start exploring a new world where conventions from functional languages are becoming more popular in C#.

This is why I hate C# :) They keep adding ways to skin a cat, and of course the old ways never go away. In the end there are no conventions for anything, a perfectionist programmer can sit for hours deciding which method is "nicer," and a new programmer has to learn everything before being able to read others' code.
– Aleksandr DubinskyJun 4 '18 at 9:42

24

@AleksandrDubinsky, You hit upon a core problem with programming languages in general, not just C#. New languages are created, lean, fresh and full of modern ideas. And very few folk use them. Over time, they gain users and new features, bolted on top of old features that can't be removed because it would break existing code. The bloat grows. So folk create new languages that are lean, fresh and full of modern ideas...
– David ArnoJun 4 '18 at 9:46

7

@AleksandrDubinsky don't you hate it when they add now features from the 1960's.
– ctrl-alt-delorJun 4 '18 at 14:06

6

@AleksandrDubinsky Yeah. Because Java tried to add a thing once (generics), they've failed, we now have to live with the horrible mess that resulted so they've learned the lesson and quit adding anything at all : )
– Agent_LJun 4 '18 at 16:12

I think it's perfectly reasonable to use a DU to return errors, but then I would say that as I wrote OneOf :) But I would say that anyway, as it's a common practice in F# etc (e.g. the Result type, as mentioned by others).

I don't think of the Errors I typically return as exceptional situations, but instead as normal, but hopefully rarely encountered situations which may, or may not need to be handled, but should be modelled.

Throwing exceptions for these errors seems like overkill - they have a performance overhead, aside from the disadvantages of not being explicit in the method signature, or providing exhaustive matching.

To what degree OneOf is idiomatic in c#, that's another question. The syntax is valid and reasonably intuitive. DUs and rail-oriented-programming are well known concepts.

I would save Exceptions for things which indicate something is broken where possible.

What you are saying is that OneOf is about making the return value richer, like returning a Nullable. It replaces exceptions for situations that aren't exceptional to begin with.
– Aleksandr DubinskyJun 6 '18 at 9:11

Yes - and provides an easy way do do exhaustive matching in the caller, which isn't possible using an inheritance based Result hierarchy.
– mcintyre321Jun 6 '18 at 12:56

OneOf is about equivalent to checked exceptions in Java. There are some differences:

OneOf doesn't work with methods producing methods called for their side effects (as they may be meaningfully called and the result simply ignored). Obviously, we all should try to use pure functions, but this isn't always possible.

OneOf provides no information about where the problem happened. This is good as it saves performance (thrown exceptions in Java are costly because of filling the stack trace, and in C# it'll be the same) and forces you to provide sufficient information in the error member of OneOf itself. It's also bad as this information may be insufficient and you can have a hard time finding the problem origin.

OneOf and checked exception have one important "feature" in common:

they both force you to handle them everywhere in the call stack

This prevents your fear

In C#, ignoring exceptions does not produce a compile error, and if they are not caught they just bubble up and crash your program.

but as already said, this fear is irrational. Basically, there's usually a single place in your program, where you need to catch everything (chances are you'll use a framework doing it already). As you and your team typically spend weeks or years working on the program, you won't forget it, will you?

The big advantage of ignorable exceptions is that they can be handled wherever you want to handle them, rather then everywhere in the stack trace. This eliminates tons of boilerplate (just look at some Java code declaring "throws ...." or wrapping exceptions) and it also makes your code less buggy: As any method may throw, you need to be aware of it. Fortunately, the right action is usually doing nothing, i.e., letting it bubble up to a place where it can be reasonable handled.

+1 but why does OneOf not work with side effects? (I guess it is because it will pass a check if you don't assign the return value, but as I don't know OneOf, nor much C# I am not sure - clarification would improve your answer.)
– dcorkingJun 5 '18 at 6:37

1

You can return error info with OneOf by making the returned error object contain useful information e.g. OneOf<SomeSuccess, SomeErrorInfo> where SomeErrorInfo provides details of the error.
– mcintyre321Jun 5 '18 at 9:04

@dcorking Edited. Sure, if you ignored the return value, then you know nothing about what problem happened. If you do this with a pure function, it's fine (you just wasted time).
– maaartinusJun 5 '18 at 11:57

2

@mcintyre321 Sure, you can add information, but you have to do it manually and it's subject to laziness and forgetting. I guess, in more complicated cases, a stack trace is more helpful.
– maaartinusJun 5 '18 at 12:02

Is using OneOf instead of exceptions for handling "ordinary"
exceptions (like File Not Found etc) a reasonable approach or is it
terrible practice?

It's worth noting that I've heard that exceptions as control flow are
considered a serious antipattern, so if the "exception" is something
you would normally handle without ending the program, isn't that
"control flow" in a way?

Errors have a number of dimensions to them. I identify three dimensions: can they be prevented, how often they occur, and whether they can be recovered from. Meanwhile, error signaling has mainly one dimension: deciding to what extent to beat the caller over the head to force them to handle the error, or to let the error "quietly" bubble up as an exception.

The non-preventable, frequent, and recoverable errors are the ones that really need to be dealt with at the call site. They best justify forcing the caller to confront them. In Java, I make them checked exceptions, and a similar effect is achieved by making them Try* methods or returning a discriminated union. Discriminated unions are especially nice when a function has a return value. They are a much more refined version of returning null. The syntax of try/catch blocks isn't great (too many brackets), making alternatives look better. Also, exceptions are slightly slow because they record stack traces.

The preventable errors (which were not prevented because of programmer error/negligence) and non-recoverable errors work really well as ordinary exceptions. Infrequent errors also work better as ordinary exceptions since a programmer may often weigh it not worth the effort to anticipate (it depends on the purpose of the program).

An important point is that it is often the use site that determines how a method's error modes fit along these dimensions, which should be kept in mind when considering the following.

In my experience, forcing the caller to confront error conditions is fine when errors are non-preventable, frequent, and recoverable, but it gets very annoying when the caller is forced to jump through hoops when they don't need or want to. This may be because the caller knows the error won't happen, or because they don't (yet) want to make the code resilient. In Java, this explains the angst against frequent use of checked exceptions and returning Optional (which is similar to Maybe and was recently introduced amid much controversy). When in doubt, throw exceptions and let the caller decide how they want to handle them.

Lastly, keep in mind that the most important thing about error conditions, far above any other consideration such as how they are signaled, is to document them thoroughly.

The other answers have discussed exceptions vs error codes in sufficient detail, so I would like to add another perspective specific to the question:

OneOf is not an error code, it's more like a monad

The way it is used, OneOf<Value, Error1, Error2> it is a container that either represents the actual result or some error state. It's like an Optional, except that when no value is present, it can provide more details why that is.

The main problem with error codes is that you forget to check them. But here, you literally can not access the result without checking for errors.

The only problem is when you don't care about the result.
The example from OneOf's documentation gives:

One thing you need to consider is that code in C# generally isn't pure. In a code-base full of side-effects, and with a compiler that doesn't care about what you do with a return value of some function, your approach can cause you considerable grief. For example, if you have a method that deletes a file, you want to make sure the application notices when the method failed, even though there's no reason to check any error code "on the happy path". This is a considerable difference from functional languages that require you to explicitly ignore return values you're not using. In C#, return values are always implicitly ignored (the only exception being property getters IIRC).

The reason why MSDN tells you "not to return error codes" is exactly this - nobody forces you to even read the error code. The compiler doesn't help you at all. However, if your function is side-effect free, you can safely use something like Either - the point is that even if you ignore the error result (if any), you can only do that if you don't use the "proper result" either. There's a few ways how you can accomplish this - for example, you might only allow "reading" the value by passing a delegate for handling the success and the error cases, and you can easily combine that with approaches like Railway Oriented Programming (it does work just fine in C#).

int.TryParse is somewhere in the middle. It's pure. It defines what the values of the two results are at all times - so you know that if the return value is false, the output parameter will be set to 0. It still doesn't prevent you from using the output parameter without checking the return value, but at least you're guaranteed what the result is even if the function fails.

But one absolutely crucial thing here is consistency. The compiler isn't going to save you if somebody changes that function to have side-effects. Or to throw exceptions. So while this approach is perfectly fine in C# (and I do use it), you need to ensure that everyone on your team understands and uses it. Many of the invariants required for functional programming to work great aren't enforced by the C# compiler, and you need to ensure they are being followed yourself. You must keep yourself aware of the things that break if you don't follow the rules that Haskell enforces by default - and this is something you keep in mind for any functional paradigms you introduce to your code.

The correct statement would be Don't use error codes for exceptions!And don't use exceptions for non-exceptions.

If something happens that your code cannot recover (i.e. make it work) from, that is an exception. There is nothing your immediate code would do with an error code except handing it upwards to log or perhaps provide the code to the user in some way. However this is exactly what exceptions are for - they deal with all the handing around and help to analyze what actually happened.

If however, something happens, like invalid input that you can handle, either by reformatting it automatically or by asking the user to correct the data (and you thought of that case), that is not really an exception in the first place - as you expect it. You can feel free to handle those cases with error codes or any other architecture. Just not exceptions.

(Note that I'm not very experienced in C#, but my answer should hold in the general case based on the conceptual level of what exceptions are.)

I fundamentally disagree with this answer's premise. If language designers did not intend exceptions to be used for recoverable errors, the catch keyword would not exist. And since we're talking in a C# context, it's worth noting that the philosophy espoused here is not followed by the .NET framework; perhaps the simplest imaginable example of "invalid input that you can handle" is the user typing in a non-number in a field where a number is expected... and int.Parse throws an exception.
– Mark AmeryJun 4 '18 at 11:42

@MarkAmery For int.Parse it is nothing it can recover from nor anything it would expect. The catch clause is typically used to avoid complete failure from an outside view, it won't ask the user for new Database credentials or the like, it will avoid the program crashing. It's a technical failure not an application control flow.
– Frank HopkinsJun 4 '18 at 12:13

3

The issue of whether it's better for a function to throw an exception when a condition occurs depends upon whether the function's immediate caller can usefully handle that condition. In cases where it can, throwing an exception which the immediate caller must catch represents extra work for the author of the calling code, and extra work for the machine processing it. If a caller isn't going to be prepared to handle a condition, however, exceptions alleviate the need to have the caller explicitly code for it.
– supercatJun 4 '18 at 15:04

@Darkwing " You can feel free to handle those cases with error codes or any other architecture". Yes, you are free to do that. However you do not get any support from .NET for doing so, since the .NET CL itself uses exceptions instead of error codes. So not only are you in a lot of cases being forced to catch the .NET exceptions and return your corresponding error code instead of letting the exception bubble up, you are also forcing others on your team into another error handling concept that they have to use in parallel with exceptions, the standard error handling concept.
– AleksanderJun 4 '18 at 17:41

It's terrible because, at least in .net, you don't have an exhaustive list of possible exceptions. Any code can possibly throw any exception. Especially if you are doing OOP and could be calling overridden methods on a sub type rather than the declared type

So you would have to change all methods and functions to OneOf<Exception, ReturnType>, then if you wanted to handle different exception types you would have to examine the type If(exception is FileNotFoundException) etc

try catch is the equivalent of OneOf<Exception, ReturnType>

Edit ----

I am aware that you propose returning OneOf<ErrorEnum, ReturnType> but I feel this is somewhat a distinction without a difference.

You are combining return codes, expected exceptions and branching by exception. But the overall effect is the same.