103

When sending a request to another module and expecting a result, it seems to me there are two ways of dealing with the 'non-happy paths'.

  • Throw an exception
  • Return a result object that wraps different results (such as value and error)

I would say the first one seems better in general. It keeps the code clean and readable. If you expect the result to be correct, just throw an exception to handle happy path divergence.

But what when you have no clue what the result will be?

For example calling a module that validates a lottery ticket. The happy path would be that you won, but it probably won't be. (As pointed out by @Ben Cottrell in the comments, "not winning" is also the happy path, maybe not for the end user, though)

Would it be better to consider that the happy path is getting a result from the LotteryTicketValidator and just handle exceptions for when the ticket could not be processed?

Another one could be user authentication when logging in. Can we assume that the user entered the correct credentials and throw an exception when the credentials are invalid, or should we expect to get some sort of LoginResult object?

helb
  • 1,380
  • 8
  • 14
dumazy
  • 1,179
  • 2
  • 8
  • 10
  • 109
    *"the happy path would be that you won"* -- No, that's not what the term *happy path* means. A losing ticket is very much 'happy path' because it's a scenario you should expect to handle in the business logic. Happy Path means that nothing unexpected happens - for example, locked files on disk, database exceptions, network interruptions, internal errors in 3rd-party libraries or API, expired certificate, etc. User auth is the same - a user's invalid username or password is also Happy path. In fact, any kind of user input validation is 'happy path'. – Ben Cottrell Feb 12 '20 at 08:38
  • True, I took a wrong turn with the lottery example. I'll add an edit to the post about it. Although I'm not sure if I agree with invalid credentials being part of the happy path. Looking at a few frameworks, they all tend to throw exceptions for invalid credentials. Even HTTP standards suggest "throwing" a 401 Unauthorized. – dumazy Feb 12 '20 at 08:46
  • 25
    @dumazy 401 is an HTTP response status code. Not an application exception. They are two very distinct concepts. Users entering incorrect passwords is not an exception scenario. – Ant P Feb 12 '20 at 08:47
  • 12
    This question is somewhat language specific though, iirc Python encourages use of exceptions in normal control flow. – Ant P Feb 12 '20 at 08:48
  • 1
    @dumazy - invalid credentials for an app connecting to a framework or API endpoint isn't the same as a user typing their username and password incorrectly. The difference is whether user input validation is involved. Users generally wouldn't be entering credentials for an app to connect to an API, but they would be typing their credentials for logging into their social media account or online banking for example. In other words, it's important to consider the origin of the thing you're checking and whether you can control or trust that source – Ben Cottrell Feb 12 '20 at 08:49
  • Alright, thanks a lot. I think it whether or not you have control of the thing definitely plays part in this. This cleared things up a bit for me – dumazy Feb 12 '20 at 09:15
  • 9
    https://ericlippert.com/2008/09/10/vexing-exceptions/ – R. Schmitz Feb 12 '20 at 16:48
  • 2
    What typically happens if an exception is not handled? Program termination. Do you want statements that can cause program termination littered throughout your code? There are two different types of non-happy paths. Sad and Bad. Exceptions should only be used on the 'bad' path. The example you provided i.e invalid login is a sad path and should be handled without using exceptions. In most languages use of exceptions to handle sad path cases would be considered bad form. – Mike Seeds Feb 12 '20 at 17:21
  • 47
    Ironically, does this mean if you build a lottery system and you win the lottery, you should throw a `LotteryWonException`, because you never expect to win? – Matthew Feb 12 '20 at 18:06
  • 1
    @AntP Correct, in some places it's even built in [at a very fundamental level](https://docs.python.org/3/library/exceptions.html#StopIteration) – Izkata Feb 12 '20 at 22:31
  • 3
    Winning or not winning or both *expected* states of validation of a lottery ticket, none of those two is *exceptional*. – Polygnome Feb 13 '20 at 08:32
  • @MikeSeeds *What typically happens if an exception is not handled? Program termination.* I consider that a feature. Often, when something *exceptional* happens there's no point in continuing. When you want to cook spaghetti and discover that you don't have pasta is there any reason to boil pot of water? No, the correct action is "program termination". The essence of raising of exception is cancelling subsequent operations. If a failure means that there's no point in executing subsequent actions, raise an exception. – el.pescado - нет войне Feb 13 '20 at 10:37
  • 1
    ... If continuing makes sense even though there's some exceptional situation, use other error handling mechanism. – el.pescado - нет войне Feb 13 '20 at 10:38
  • @Matthew: It seems to be the exception used by casinos when they don't feel like [shelling out $42 million](https://www.courthousenews.com/casino-sued-downgrading-jackpot-steak-dinner/). – Eric Duminil Feb 13 '20 at 10:48
  • 4
    Does this answer your question? [Return magic value, throw exception or return false on failure?](https://softwareengineering.stackexchange.com/questions/159096/return-magic-value-throw-exception-or-return-false-on-failure) – curiousdannii Feb 13 '20 at 12:24
  • It's in the context of JavaScript (specifically Node.js), but there's a decent discussion of handling different types of errors here: https://www.joyent.com/node-js/production/design/errors – Paul D. Waite Feb 13 '20 at 12:32
  • 1
    [Related question](https://stackoverflow.com/q/54713231/1835769), asked on SO, and [my answer](https://stackoverflow.com/a/54720130/1835769). – displayName Feb 13 '20 at 17:22
  • @BenCottrell Nope, a happy path is a single path, typically understood to be the most simple most common path that is expected to go through without error or exception being thrown / required to be handled. It is ONE path, not all that go through, and there may be additional alternative paths that also run without exception. See for instance Wikipedia: "In use case analysis, there is only one happy path, but there may be any number of additional alternate path scenarios which are all valid optional outcomes." https://en.wikipedia.org/wiki/Happy_path – Frank Hopkins Feb 13 '20 at 21:43
  • 2
    As noted, this is very language-specific. Some languages don't have exceptions at all. Some do, but still prefer not to use them. – Sebastian Redl Feb 14 '20 at 13:10
  • 1
    @BenCottrell, `Happy Path means that nothing unexpected happens - for example, locked files on disk, database exceptions [...]` - I don't see the difference. If you know these things can happen, then they're not unexpected? They're rare, but not unexpected. So the question is rather how rare must a thing be to be thrown as an exception instead of being handled as a return value? – Peppe L-G Feb 19 '20 at 10:02
  • So basically your language has a built in standard mechanism for reporting unexpected behavior (`exceptions`), but you think returning a domain specific object is a valid choice? While you're at it why don't you just use character arrays instead of strings. – The Muffin Man Feb 21 '20 at 23:46

12 Answers12

85

You have to distinguish between return values and errors.

A return value is one of many possible outcomes of a computation. An error is an unexpected situation which needs to be reported to the caller.

A module may indicate that an error occurred with a special return value or it throws an exception because an error was not expected. That errors occur should be an exception, that's why we call them exceptions.

If a module validates lottery tickets, the outcome may be:

  • you have won
  • you have not won
  • an error occurred (e.g. the ticket is invalid)

In case of an error, the return value is neither "won" nor "not won", since no meaningful statement can be made when e.g. the lottery ticket is not valid.

Addendum

One might argue that invalid tickets are a common case and not an error. Then the outcome of the ticket validation will be:

  • you have won
  • you have not won
  • the ticket is invalid
  • an error occurred (e.g. no connection to the lottery server)

It all depends on what cases you are planning to support and what are unexpected situations where you do not implement logic other than to report an error.

helb
  • 1,380
  • 8
  • 14
  • 20
    Surely 'Invalid ticket' would be a normal part of validation and therefore something which should be expected rather than an error per-se. For example, a user could quite legitimately present the system with the wrong barcode, type the wrong ticket number, enter the number for an expired ticket, enter a number for a ticket which has already paid out winnings, etc. Unexpected errors would be things like network failure or a back-end service becoming unavailable – Ben Cottrell Feb 12 '20 at 10:15
  • @BenCottrell Well, my example for an error case may not be the best, see the "Addendum" section. – helb Feb 12 '20 at 10:18
  • 1
    "Server performing the ticket validation is down" would be an exception. Well, we hope it's an exception :-( – gnasher729 Feb 12 '20 at 13:24
  • 43
    Ahh but then one could argue that "because the shop is in a remote location with flaky cell/net coverage, not being able to contact the server is every bit as expected an error as the user typing in an invalid ticket number etc.." and so it goes on, down into the navel fluff of every possible thing. I would thus say it's not so much about unexpected conditions, but about only covering for a reasonable number of conditions before one reaches the point of throwing the towel in.. – Caius Jard Feb 12 '20 at 17:27
  • 10
    Oh, it wasn't a criticism, it was more that your last sentence said "what are *unexpected* situations" whereas I'm advocating that it may not be so much about expected/unexpected, it's about reasonable-to-code-for/unreasonable-to-code-for - I can fully expect the lottery server being unavailable, but the judgement call of whether to code for it (or any of the other hundred things I can "expect" to go wrong) or not is the threshold of "how much defense for situation X do we want to go to the effort of writing". – Caius Jard Feb 12 '20 at 18:08
  • (You cover this in "cases you're planning to support" but "unexpected situations" isn't, to me, necessarily the flip side of that coin) – Caius Jard Feb 12 '20 at 18:10
  • 1
    @CaiusJard My thinking on that topic would be around separation-of-concerns between core/domain logic and logic around the "boundaries" related to infrastructure/OS/APIs/etc. I think it's important to make a clear distinction between validation in domain data/logic versus failures/errors happening in external dependencies. Even if infrastructure failures are expected, I would still regard them as "unexpected" from the domain point of view. Exceptions are useful because they unwind the stack without the domain logic needing to care, whereas domain logic probably does care about bad data. – Ben Cottrell Feb 12 '20 at 20:53
  • @CaiusJard while I do agree that "unexpected situations" might not be the correct way to describe them, I would also argue that neither is "reasonable to code for". Exceptions should be about things that happen that are outside of the scope or the normal functioning of the process. Yes, it might be reasonable to expect the server to be down, but "being down" is not part of the functions the server should do. – Josh Part Feb 12 '20 at 21:43
  • 1
    @JoshPart - You can code for them by writing an exception handler. Throwing exceptions doesn't mean "not coding for" - it means delegating the decision about how to code for them to the calling code. The lottery ticket checking function has no idea what the most appropriate response is for handling 'unable to connect' errors - that may be several layers higher, maybe even all the way to the user ("Unable to connect - try again?"). Better to throw an exception so that the appropriate layer can pick it up without the intervening layers needing to do anything special. – HappyDog Feb 13 '20 at 00:16
  • @CaiusJard In that case, I guess you would design the interaction to avoid checking the ticket if there is no connection. IMHO it all boils down to the actual task you want to perform and the preconditions you set that must be verified. Return values communicate legit outcomes when preconditions are satified, exceptions communicate when the preconditions are broken. (e.g. I assume °I have connection to check the ticket, but I do not assume the ticket is valid yet) – bracco23 Feb 13 '20 at 08:56
  • 'It all depends on what cases you are planning to support', so crucial to this answer. If confused go back to your requirements. – Rohan Bhale Feb 13 '20 at 12:04
  • The "define errors out of existence" heuristic suggests that in many cases, "invalid ticket" might just be "did not win". The "do not leak information unless you know it is secure (and if your first reaction is 'how can it be not secure?' you don't know if it is secure)" heuristic also suggests that unless there is a good reason otherwise, an "invalid ticket" should produce the same result *to the user* as "did not win". UI / UX best practices suggest that in both "did not win" and "invalid ticket" cases, if manual entry was involved, prompt the user to double check if they entered it right. – mtraceur Feb 14 '20 at 01:37
  • This distinction is something I have been struggling with this week with gRPC. It strongly feels to me that gRPC is using exceptions for program flow. – Dave Nay Feb 15 '20 at 15:04
  • I'd argume that "Invalid Ticket" (as in bad values such as out of range numbers, expired date, bad scan etc) does seem like an exception and in many languages function do throw `ArgumentOutOfRange` or `ArgumentFormat`-like exception to indicate exactly that. That's exactly what good and secure programing practices dictate - validate function input validity and in case of invalid input to reject processing the call (i.e. often by throwing exception, language and stylistics dependent). – LB2 Feb 20 '20 at 23:45
  • So how does this work when there is middleware involved? As a developer I dont know which middleware is injected which can throw exceptions, can we assume all middleware should add its exceptions as an item to a result? – Wilko van der Veen Nov 23 '22 at 07:28
  • I don't like traditional exception definition. `an error occurred (e.g. the ticket is invalid)` but caller must know about this exception for example EInvalidTicket. So if caller knows about all exceptions, they are no longer exceptions, because caller `catch EInvalidTicket` and processes this situation, which is similar to return a Tuple with nullable value and error enumeration. – iperov Jan 05 '23 at 15:26
70

This is a good question that professional developers have to consider carefully. The guideline to follow is that exceptions are called exceptions because they are exceptional. If a condition can be reasonably expected then it should not be signaled with an exception.

Let me give you a germane example from real code. I wrote the code which does overload resolution in the C# compiler, so the question I faced was: is it exceptional for code to contain overload resolution errors, or is it reasonably expected?

The C# compiler's semantic analyzer has two primary use cases. The first is when it is "batch compiling" a codebase for, say, a daily build. The code is checked in, it is well-formed, and we're going to build it in order to run test cases. In this environment we fully expect overload resolution to succeed on all method calls. The second is you are typing code in Visual Studio or VSCode or another IDE, and you want to get IntelliSense and error reports as you're typing. Code in an editor is almost by definition wrong; you wouldn't be editing it if it were perfect! We fully expect the code to be lexically, syntactically and semantically wrong; it is by no means exceptional for overload resolution to fail. (Moreover, for IntelliSense purposes we might want a "best guess" of what you meant even if the program is wrong, so that the IDE can help you correct it!)

I therefore designed the overload resolution code to always succeed; it never throws. It takes as its argument an object representing a method call and returns an object which describes an analysis of whether or not the call is legal, and if not legal, which methods were considered and why each was not chosen. The overload resolver does not produce an exception or an error. It produces an analysis. If that analysis indicates that a rule has been violated because no method could be chosen, then we have a separate class whose job it is to turn call analyses into error messages.

This design technique has numerous advantages. In particular, it allowed me to easily unit-test the analyzer. I feed it inputs that I've analyzed "by hand", and verify that the analysis produced matches what I expected.


Can we assume that the user entered the correct credentials and throw an exception when the credentials are invalid, or should we expect to get some sort of LoginResult object?

Your question here is about what I think of as "Secure Code" with capitals. All code should be secure code, but "Secure Code" is code that directly implements aspects of a security system. It is important to not use rules of thumb/tips and tricks/etc when designing Secure Code because that code will be the direct focus of concerted attacks by evildoers who seek to harm your user. If someone manages to sneak wrong code past my overload resolution detector, big deal, they compile a wrong program. But if someone manages to sneak past the login code, you have a big problem on your hands.

The most important thing to consider when writing Secure Code is does the implementation demonstrate resistance to known patterns of attack, and that should inform the design of the system.

Let me illustrate with a favourite example. Fortunately this problem was detected and eliminated before the first version of .NET shipped, but it did briefly exist within Microsoft.

"If something unexpected goes wrong in the file system, throw an exception" is a basic design principle of .NET. And "exceptions from the file system should give information about the file affected to assist in debugging" is a basic design principle. But the result of these sensible principles was that low-trust code could produce and then catch an exception where the message was basically "Exception: you do not have permission to know the name of file C:\foo.txt".

The code was not initially designed with a security-first mindset; it was designed with a debuggability-first mindset, and that often is at cross-purposes to security. Consider that lesson carefully when designing the interface to a security system.

Eric Lippert
  • 45,799
  • 22
  • 87
  • 126
  • 3
    While I like your post, I think with regards to the exceptions Microsoft erred on the wrong side. MOST system do not need to be as hardened to attack that almost any exception needs to be useless to aid in error correction. The should have been a switch to neuter exceptions, but it should have been off by default. – Christian Sauer Feb 13 '20 at 07:47
  • 8
    @ChristianSauer: If you've jumped through the hoops of intentionally running code as low-trust, you probably **do** want that level of hardening. – Brian Feb 13 '20 at 19:31
  • @EricLippert: Do you think this style should be adopted in general? Why or why not? – Ryan Haney Feb 20 '20 at 19:37
  • 3
    @RyanHaney: The whole point of making a *general-purpose multi-paradigm* programming language is to *not* lock developers into "one right way" to structure an application; rather, architecture should follow purpose. I write code analyzers for a living; it make sense for me to structure analyzers such that they *produce an analysis*. It's a good paradigm and it solves a lot of problems, but I certainly would not dogmatically say that it ought to be applied in general, irrespective of business domain or program architecture. – Eric Lippert Feb 20 '20 at 20:32
51

I'm going to take a slightly different track (I hope) than the other answers. A method should throw an exception when it's unable to fulfill its contract, which is based on how you name the method. The other answers say "exceptions for exceptional conditions," which I think can lead to some questionable design choices in some situations.

There are times where it can be hard to say if something which happens during the execution of a method should be rare (and thus exception) or a normal result path. Thinking about "did the method perform what its intended function" helps clarify things, IMO.

For example, the implementation uses a web service. Is it normal that service call might fail or be unavailable, or is that an exception case? Does it matter if you know that the service is very reliable (almost always able to be called successfully, in which case you'd say failure is exceptional and thus should throw) or if you know that the service fails frequently (which could lead you to a result code design)? The answer is that for your API, it doesn't matter; the service is an implementation detail and users of your API shouldn't care about HOW the validate method works, only that the method was able to validate (thus a result), or not (thus throws).

So a ValidateLotteryTicket method should return a result if the ticket is a winner, if its not a winner, or if the ticket number is invalid. It should throw an exception if something prevents it from actually validating the ticket, perhaps due a network error, the host process is shutting down, or the host machine is out of memory to continue. The validate method should only return if it was able to perform the validation and come to a conclusion.

A method to Login should throw if it is unable to log the user in; again, perhaps there's a network error preventing the credential validation, maybe the account is locked, maybe the password is invalid, or even the system was unable to log a login audit record after successfully validating the credentials. The expectation when Login is called and returns is that the user is now logged in with appropriate privileges assigned. If Login cannot exit in that state, it should throw an exception.

Andy
  • 2,003
  • 16
  • 22
  • 6
    Absolutely. Another way of wording this is "The method is unable to fulfil the contract established by its name". If the method's name says that it will validate the lottery ticket, it should show an exception if it is unable to fulfil that contract: if it is unable to validate the lottery ticket. Then of course you have the discussion around what "validate" actually means in your domain... – canton7 Feb 13 '20 at 09:29
  • 1
    @canton7 Excellent way of wording it; I wish there were more emphasis on contracts. I'll edit your wording into my answer. – Andy Feb 13 '20 at 15:30
  • 2
    @canton7 Method names are almost never sufficient documentation. Contracts should be documented in doc comments or the language's equivalent. – StackOverthrow Feb 13 '20 at 16:51
  • I like this, as it isn't based on how rare the exceptional case might be. It also means that a short method name like `pay` should have exceptions declared on it for insufficient balance or invalid account, or it should be named `payIfSufficientBalanceAndAccountValid` with an appropriate result type detailing the final result. – john16384 Feb 15 '20 at 16:38
  • It took me a little while before I understood what you were saying about the Login method. I think your answer would benefit if you compared it with a method to validate credentials. – Wes Toleman Feb 16 '20 at 02:42
  • @WesToleman I'm not sure I follow; what do you think the Login method looks like vs ValidateCredentials? – Andy Feb 16 '20 at 16:55
  • @Andy, I've been mulling it over, maybe the distinction isn't as important as I first thought. I have a bias against exceptions so I try to engineer them out. `Login` logs in or excepts, `ValidateCredentials` validates credentials (returning a bool or some sort of credential validation result) or excepts. In the case of validation the only exceptional case is "I don't know because I couldn't reach the credential store" rather than anything to do with the input itself. – Wes Toleman Feb 18 '20 at 01:21
  • @westoleman Exceptions were created for a good reason; you can ignore return codes, you can't ignore exceptions (without crashing). In my thinking Login would put the state of the app into an authenticated state but Validate wouldn't. For that reason i think Login should throw in invalid creds (since it cant put the app in the expected state) but Validate could return true or false since you're only asking if the creds are valid. Does that match your thinking? – Andy Feb 18 '20 at 01:59
  • 2
    @Andy, yes that's exactly the distinction. Avoiding exceptions for me isn't about exceptions vs return codes (which you can enforce to some degree in some languages with attributes like `[[nodiscard]]`). It's more about turning turning run-time issues into compile-time errors and making it impossible to represent invalid states. Some exceptions are inevitable however. Philosophically, I don't like the scoping issues `try`/`catch` introduces and the longjump nature of exceptions isn't much different to `goto` yet it seems to get a free pass in that regard. – Wes Toleman Feb 18 '20 at 03:08
  • I have to disagree on the last part of the answer. Login should be able to fail without an exception. Just as lottery tickets will not always win the login could 'win' or 'loose' or maybe invalid (but catching say empty input would be part of the frontend). Exceptions only when the connection to the login validation process fails. – Paul Palmpje Feb 20 '20 at 20:02
  • @PaulPalmpje My answer is based in the semantics of the method name and what the method name implies. Login should throw if its not able to put the system into a logged-in state. But that doesn't mean you can't create a TryLogin method that returns a true or false based on the result. But you might **still** need to handle exceptions from a TryLogin; what if the system can't log you in b/c your app is in maintenance mode (and you need to tell users that)? Now false is "bad user/pwd" or "system in maint." or "i don't know what went wrong." – Andy Feb 20 '20 at 20:58
  • @Andy Yes. I agree in part. Mostly my view is that exceptions are just that. You can expect a login to fail and the consumer process should handle that in a normal way. Expanding this a bit... the login method should return a state value and only throw in really bad circumstances. – Paul Palmpje Feb 20 '20 at 21:07
  • 1
    @PaulPalmpje Well, that's not really the point of my answer, which is why i said its a different take and not "exceptions only for exceptional reasons," and we have a few answers along those lines already, I just wanted to provide a different perspective which I think is also valid. Of course you don't need to agree. – Andy Feb 21 '20 at 00:47
  • I do a lot of Ruby/Rails stuff and I basically agree with this interpretation: as an example: a service object called `UserCreator`, which is expected to return an object which represents the user which was created. If the user wasn't created, then a common approach is to return `nil` or `false`, or a result object which responds to a `success?` message with `false` but contains no data. All these approaches involve returning different-shaped responses (/different types) in different cases. Better to return an object of one shape, and to raise an exception if that's not possible. – dgmstuart Feb 23 '20 at 18:58
  • I believe this to be the correct approach and the accepted answer in general. I would love to hear any counter-arguments or examples where this could not be applied. – Snackoverflow Mar 19 '20 at 15:27
17

This very much depends on the environment and language you are working in. (SE Stackexchange is overrun with Java programmers, and most of the answer demonstrate as much.)

There are several common techniques:

1. Return values

Go doesn't have automatically propagating errors. All error handling is explicitly handled by returning a result and an optional error.

 f, err := os.Open(path)
 if err != nil {
     log.Fatal(err)
 }

In functional languages, there is often an Either/Result monad that is used for this.

2. Out parameter

C# has output parameters. For example int.TryParse. It returns the success/failure and modifies the argument to store the resulting value.

int number;
if (int.TryParse(text, out number)) {
    Console.WriteLine("invalid");
}
Console.WriteLine(number);

C functions often do similar things, using pointers.

3. Errors in exceptional cases only

Conventional Java/C# wisdom is that errors are appropriate in "unusual" cases. This largely depends on the level you are working at.

A failure to establish a TCP connection might be an error. An failed remote authentication attempt (e.g. HTTP 401/403) might be an error. A failure to create a file due to a conflict might be an error.

try {
    socket = serverSocket.accept();
    socket.close();
} catch (IOException e) {
    System.out.println(e.getMessage());
}

There is usually a taxonomy of errors (e.g. in Java, "errors" are severe, program-threatening events, "unchecked exceptions" are indication of programmer error, and "checked exceptions" are unusual but expected possibilities).

4. Errors generally

In Python, errors are an acceptable and idiomatic form of flow control, just as much as if-then for for.

try:
    a = things[key]
except KeyError:
    print('Missing')
else:
    print(a)

I recommend finding the pattern for your language/ecosystem/project and sticking to that.

Paul Draper
  • 5,972
  • 3
  • 22
  • 37
  • 6
    Checked exceptions are not necessarily unusual. Their defining characteristic is that they represent an error that is both external (i.e., not a bug) and potentially recoverable. In other words, they're exceptions that *should* be caught by the application. Errors are external and unrecoverable; they're only catchable so applications can log them before exiting. – StackOverthrow Feb 13 '20 at 17:06
  • @user560822, idiomatically, Java exceptions are for "not normal" situations https://softwareengineering.stackexchange.com/a/189225 But they can be used in [many ways](https://xkcd.com/1188/). – Paul Draper Feb 14 '20 at 20:17
  • 2
    Java's checked exceptions were intended to signal *expected* error conditions like "file not found" that a robust program should handle. It was an attempt at adding compiler support for forcing the programmer to handle these kinds of common errors. An external error being rare was actually an argument for making what would otherwise be a checked exception into an unchecked exception. – StackOverthrow Feb 14 '20 at 20:32
  • Checked exceptions allows you to make something part of the method contract that must be dealt with, just like its result value and parameters are part of its contract -- the compiler will flag breaches of these contracts, whether it is not catching an exception, passing a `String` where an `int` is expected or assigning the result to a wrong variable type. – john16384 Feb 15 '20 at 16:43
  • Even in languages where exceptions are idiomatic (like Python), I prefer to avoid throwing them in non-exceptional cases because it makes programs a pain to debug when the debugger is breaking in frequently. – Gabe Feb 22 '20 at 05:29
  • I wrote an article about how to use C# multiple return values to be like GO (either a result or an error): https://drizin.io/using-multiple-return-values-instead-of-exceptions/ – drizin Jan 06 '21 at 14:11
8

Ultimately it is a matter of what is idiomatic in the language you are writing in.

I read through the answers and was shocked to find no mention of rust, which is the first language which comes to mind which advocates (kind of forces) the user to returns errors/null values wrapped in result objects. As in illustrative example, one such type is:

Result<T, E> is the type used for returning and propagating errors. It is an enum with the variants, Ok(T), representing success and containing a value, and Err(E), representing error and containing an error value.

chris
  • 269
  • 1
  • 5
  • I'm not familiar with rust; what if the result contains an error, does it somehow force you to handle the error if there is one, or can it be ignored? – Andy Feb 13 '20 at 15:46
  • 2
    @Andy This pattern is commonly used in functional programming languages, too You are generally free to ignore it just like you are free to do nothing in an exception handler. However, APIs tend to be written in such a way that ignoring the return value doesn't make any sense, and you'll generally be using pattern matching (which force you to explicitly propagate or handle) and/or some monadic operations (which will propagate errors nicely). – gustafc Feb 13 '20 at 16:48
  • 2
    The nice thing with this approach, is that you can handle result values just like you handle any other value in the programming language - you're not forced to handle an error in the same call stack as it happened. This becomes very handy when you're doing map/flatMap/filter/reduce chains on collections, for example. – gustafc Feb 13 '20 at 16:56
  • "first language that comes to mind" Haskell came to my mind before Rust was even a twinkle. And C as well, though the error paradigms for C are varied. – Paul Draper Feb 14 '20 at 20:20
  • @PaulDraper I'm too young – chris Feb 24 '20 at 07:56
4

I'd put it the other way around and consider an exception (or Throwable in Java terms) a specially treated return object (as it is optional to throw) with one very distinctive feature.

It has a stack trace attached to it!

A stack trace used to be a very expensive ting to generate by the JVM (it has become better) but this was more than outweighed by its usefulness to the programmer reading it to debug the program. This is also why chained exceptions were added to Java 1.4.

So the question I think you should be asking yourself is:

Do I need a stack trace for this situation?

If the answer is not a resounding yes, then don't. Use a more suited data structure.

  • 2
    Java atleast allows you create exceptions without stacktraces and avoid the penalty (if that isn't a case of premature optimization in the first place) by disable `writableStrackTrace`. You can also create exceptions that are static (if the exception class is sufficient information to identify the problem) by making the exception immutable (`writableStackTrace` and `enableSuppression` both set to `false`) – john16384 Feb 15 '20 at 17:04
  • 1
    @john16384 Interesting. Appears to have been added in Java 7, but I cannot find out _why_. Doing this will in my opinion sacrifice clarify for no good (to me) reason. – Thorbjørn Ravn Andersen Feb 15 '20 at 17:12
3

Exceptions cause you problems when you work with callbacks. It's very often that you call some code, it does things in the background, then calls the callback, and it is really helpful to your sanity if you know the callback will be called, no matter what happens (for example what errors happened). Very often the caller is long gone by the time you find a reason to throw an exception.

In that case, returning error + result is much easier to handle correctly. In Swift, an enum containing two cases for error and result is actually part of the standard library for exactly that reason.

gnasher729
  • 42,090
  • 4
  • 59
  • 119
  • 2
    Although I think many languages today do "background stuff" via async await, and exceptions which occur in the async part are thrown when the async operation is awaited. – Andy Feb 13 '20 at 00:34
  • 1
    Scala has a similar idiom with its `Either` type being used to represent return values which can either be a normal result or an error. – Christian Hackl Feb 13 '20 at 07:13
  • The callback is simply poorly written if it doesn't catch exceptional cases (and either handles them, logs them or wraps them). Even functions which donot throw documented exceptions can throw exceptions at any time if they're dealing with any kind of I/O for example. – john16384 Feb 15 '20 at 16:45
  • Ohm, I think you don’t quite understand the problem. If the function throws an exception, who is going to call the callback? – gnasher729 Feb 19 '20 at 16:05
  • @gnasher729 it seems to me that there is not much difference between throwing an exception and returning an error prematurely. If a callback has to happen, any code that can throw or return an error needs to be wrapped. – Codebling Feb 20 '20 at 07:20
3

The dichotomy isn't between "exceptions" and "results" objects.

In reality, it's in the distinction between unexpected or fatal VS anticipable or recoverable errors, and whether the calling program can reasonably do something to change the outcome when the error is reported (as opposed to requiring a program change).

This distinction has gotten muddied by many modern languages that set both types (unexpected/recoverable errors) up as "exceptions":

  1. Attempting to parse an integer to a string fails if there are no digits in the string data, yielding an ArgumentInvalidException.
  2. Attempting to parse an integer to a string fails if there is insufficient memory to allocate the string object, yielding an OutOfMemoryException.

In the case of 1., the error is anticipable, and the program should be able to gracefully handle the case (see also Railway-Oriented Programming; note this is about handling the errors, not using an explicit Result<T, Error> object).
In the case of 2., the error is neither anticipable, nor is it (likely) recoverable. The only reasonable avenue left is to crash the program.

For a practical look at the differences, I recommend reading this blog post about the Midori error model.

Clockwork-Muse
  • 346
  • 2
  • 6
  • 1
    If code is trying to parse an integer which is going to be valid *unless an input file is sufficiently corrupted as to render any attempts at parsing meaningless*, there's really not much benefit to having the parser's immediate calling code try to handle a parse fail gracefully. Unfortunately, existing languages' exception mechanisms are poorly adapted to distinguish between scenarios where a function fails without adverse side effects, and those where failure could leave an object in a corrupted state. Proper `Dispose` handling for things like lock tokens should distinguish between... – supercat Feb 12 '20 at 23:21
  • ...cases where code that uses a token completes normally (implying the lock should be released) and cases where it exits via an exception (implying that if the token was acquired for writing, the lock should be *invalidated* such that all pending and future acquisition attempts fail instantly). Unfortunately, without a provision for that, distinguishing between recoverable and unrecoverable failures will be very difficult. – supercat Feb 12 '20 at 23:24
  • @supercat - Files being unreadable (whether due to byte twiddling or just a wrong text block) is an anticipable error. If that means your application can't process that file any further, then don't do so (or stop the program). Otherwise, if you're trying to lock the file such that **other** programs can't read it, that's _usually_ poor behavior. – Clockwork-Muse Feb 12 '20 at 23:56
  • @Clockwork-Muse it may be worth using the words *checked* and *unchecked* in your answer – Codebling Feb 20 '20 at 07:23
2

The everything returns a result model is used by COM; practically every method returns a HRESULT. Because of this, you wind up with code that looks like this:

HRESULT hr = object1.CallMethod1( ... ); 
if ( IS_ERROR( hr ) )
   return ; 

hr = object1.CallMethod2(); 
if ( IS_ERROR( hr ) )
   return ; 

hr = object1.CallMethod3( ... ); 
if ( IS_ERROR( hr ) )
   return ; 

and so on ...

It a style of coding, but it makes for a lot of code to achieve, in some cases, not very much.

The Good thing about C++:

  • You can write code to do almost anything with it.

The not so Good thing about C++:

  • You have to write code to do anything with it.

Exceptions allow you to cut through this "Stepping through the Minefield" style of coding.

Your code just keep painting the floor until you unexpectedly find yourself in a corner, at which point it throws an Exception, effectively abandoning the rest of the room and jumping out of the window, in the hope that something is going to catch it.

Phill W.
  • 11,891
  • 4
  • 21
  • 36
  • Nice analogy ;-) You may want to mention that return values in COM are usually passed with [out] parameters. – helb Feb 12 '20 at 12:17
  • @helb: I can't believe I'd actually /forgotten/ that! It /has/ been a while since I tip-toed through the Minefield ... – Phill W. Feb 12 '20 at 16:20
  • I believe OP is talking about the modern functional programming concept of the "Result" type, which is pretty significantly different than HRESULT. Most implementations of "Result" will (usually) slightly decrease the amount of code you have compared to exceptions, if you use them functionally. – Clay07g Feb 13 '20 at 02:31
  • Well-written C++ should produce less code than other languages because of the prevalence of value types, RAII and move semantics, which in turn drastically reduces exception handling at higher abstraction levels. In particular, you don't have to manually free handles returned from C APIs anymore if destructors do it for you automatically, whereas in certain other languages the code would be littered with `finally` blocks and other manual resource mangagement. Of course, COM programming itself is difficult and old-fashioned, but that has little to do with the programming language. – Christian Hackl Feb 13 '20 at 06:45
  • @ChristianHackl: Unfortunately, C++ has no good way of handling the fact that an attempt to release a resource in response to an exception may fail in ways that an application would need to know about. – supercat Feb 14 '20 at 19:29
  • @supercat: I would argue that this is not a language problem. There is often no "good" way to handle a failure to release a resource, because the lower abstraction layer does not offer one in the first place. For example, if `fclose` fails, the stream is still disassociated with the underlying file anyway, and all you can really do is log the error and/or exit the process. – Christian Hackl Feb 17 '20 at 07:19
  • @ChristianHackl: In an interactive application, if an attempt to close a file for writing fails, the most important aspect of recovery would be to tell the user "Hey, your document may not have been written successfully and you should save a copy somewhere else". I know of no nice way to accomplish that in C++. – supercat Feb 17 '20 at 15:28
  • @supercat: You can always just call the stream's `close` function manually and build some UI logic around it. Dealing with all possible filesystem errors in an interactive manner is an extremely complicated task in any case. – Christian Hackl Feb 18 '20 at 06:06
  • @ChristianHackl: True, if one foregoes RAII one doesn't have to deal with problem cases, but one also loses some of the advantages. Another scenario where languages fall short is with using RAII-style management for read/write lock tokens. If code enters a region that acquires a lock token for writing and then leaves that region because of an exception, the lock token should neither be released nor abandoned, but instead expressly invalidated so all pending or future attempts to acquire the lock fail. If the program can recover from the locked resource becoming unavailable... – supercat Feb 18 '20 at 17:53
  • ...the exception while holding the lock shouldn't be fatal, but if the program isn't going to be able to recover it should fail fast. – supercat Feb 18 '20 at 17:54
1

The question is already answered, but I think that a lot more should be said on the argument. First of all there should be a separation between technical errors and business errors. Exceptions should be used to handle technical errors, not the business ones. The primary reason is that they maybe back-propagated and disrupt the whole logic flow. It makes sense to stop processing when you have an IO Error or the network is missing because all the other services, modules, subcalls may stop working for the same reason. Furthermore a technical error is something that usually is unexpected and the developer can't implement specific paths to handle all the possible technical errors.

A business error most of the times is something well known and a proper path, to handle it, can be easily designed. Obviously with try/catch a proper path could be implemented also when there is an exception, but when different people work on the same application or maintenance is done after a long time someone could easily forget to add the required try/catch and the error could end up being improperly reported.

Another point is that business and technical errors should be reported to different people. Using the try/catch flow to handle everything makes difficult to separate them. When something like "you have not won" or "the ticket is invalid" happens the application might just send a message to the user. The above mentioned "IO error" instead might require the intervention of a sysadmin.

The only exception I see to this rule are some kind of error that are not clearly defined and the developer does not know how to handle. The most common case is when there are constraints in the input data, there could be too many ways a constraint might be violated and as a matter of fact usually in Java when it happens the input data is bounced back with an IllegalArgumentException.

FluidCode
  • 709
  • 3
  • 10
1

You should try to never return exceptions unless there is no other option. The problem with standard error handling is that you just throw something upwards, the user of the method is not informed that failure is an option by reading the function parameters, nor do we know what the return value is if we don't catch the exception.

The best thing you can do when there are multiple return value's is wrap it in a result object. When writing your listener you must handle the return object to get to the actual return value. This means every time the method is called users are forced to think about how they want to handle different result states and values. Therefore errors aren't thrown on an endless stack until it reaches the consumer with an annoying popup or an invisible failure somewhere in the background.

This kind of result handling is one of the base concepts in the Rust programming language, which doesn't use any null reference types or other unwanted behavior. I've used a similar approach in c# as Result in Rust, where a method return a Result object. If the result is OK you can reach the value, otherwise it is mandatory to handle the error. The nice thing about coding this way is that you are solving problems in your code before even testing or running it.

Example of what my code looks like:

enum GetError { NotFound, SomeOtherError, Etc }

public Result<ValueObject, GetError> GetValue() {
  if (SomeError) 
    return new Err<ValueObject, GetError>(GetError.SomeError);

  else return new Ok<ValueObject, GetError>();
}

value = GetValue()
          .OnOk( // OnOk is used to get the ok value and use it
            (ok) => {
               // do something with the ok value here...
            }
          )
          .OnErr( // OnErr is used to get the error value and act on it
            (err) => {
               // do something with the err value here...
            }
          )
          .Finalize( // finalize is used to make a value out of the result.
            (err) => {
                // if the result is ok it returns the ok value
                // if the result is an error it needs to be handle here
                return new ValueObject();
            });

As you can see, there is not much left that can go wrong. The downside is that code can become tedious at places so using results like this is not always your best option. The thing that i am trying to tell is, you as a programmer decide how your code behaves and handles error situations, and not the other way around.

  • Go also forces to return tuple with result and error. Rust allows to avoid runtime overhead for `Option` and `Result`. – gavenkoa Jul 04 '21 at 21:56
0

In short: it depends.

Taking the lottery ticket example:
bool isWinningTicket(const LotteryTicket& ticketToCheck)

One could argue that an invalid ticket is (by default) not a winning ticket, so let's look at the "validation servers unreachable" scenario. In that case the function cannot provide what it promised - a true / false reply if the ticket is a winning ticket - so it should throw an exception to make you aware of that.

Why? Let's consider the alternatives, like
bool checkIfTicketIsWinningTicket(const LotteryTicket& ticketToCheck, bool& isWinning)
Stores the true/false (if the ticket is winning) result in the out-parameter bool& isWinning and indicates success/failure of the function itself by returning true/false, no exceptions, no hassle... except (pardon the pun) - you might, by accident, forget to check the return value, miss that the function failed, and give false positives/negatives (each of which makes different people unhappy).

If you forget to catch the exception in the first example... you'll get an run-time error and that should make you aware that something is wrong with your code (unless you went out of your way to write catch(...) { /*do nothing*/ } but then no one can help you anyway). In the second case your code will fail silently, and you will only notice when either angry lottery people or angry ticket holders knock down your door...

As for the login example: If your function void loginUser(User userToLogIn, Password passwordOfUser) - promises to log valid users (i.e. people are expected to call bool isValidUser(User u, Password p) beforehand and after loginUser()is called the user will be logged in) then an exception for invalid user/password combinations is correct. If the function works on a "see if the combination is valid and log in if it is" basis, then you shouldn't throw an exception for that (and give the function a different name).

CharonX
  • 1,633
  • 1
  • 11
  • 23