107

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?

21
  • 111
    "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'. Commented Feb 12, 2020 at 8:38
  • 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
    Commented Feb 12, 2020 at 8:47
  • 12
    This question is somewhat language specific though, iirc Python encourages use of exceptions in normal control flow.
    – Ant P
    Commented Feb 12, 2020 at 8:48
  • 10
    ericlippert.com/2008/09/10/vexing-exceptions
    – R. Schmitz
    Commented Feb 12, 2020 at 16:48
  • 51
    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
    Commented Feb 12, 2020 at 18:06

12 Answers 12

87

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.

16
  • 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 Commented Feb 12, 2020 at 10:15
  • 1
    "Server performing the ticket validation is down" would be an exception. Well, we hope it's an exception :-(
    – gnasher729
    Commented Feb 12, 2020 at 13:24
  • 46
    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
    Commented Feb 12, 2020 at 17:27
  • 11
    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
    Commented Feb 12, 2020 at 18:08
  • 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. Commented Feb 12, 2020 at 20:53
73

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.

4
  • 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. Commented Feb 13, 2020 at 7: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
    Commented Feb 13, 2020 at 19:31
  • @EricLippert: Do you think this style should be adopted in general? Why or why not?
    – Ryan Haney
    Commented Feb 20, 2020 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. Commented Feb 20, 2020 at 20:32
52

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.

15
  • 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
    Commented Feb 13, 2020 at 9: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
    Commented Feb 13, 2020 at 15:30
  • 2
    @canton7 Method names are almost never sufficient documentation. Contracts should be documented in doc comments or the language's equivalent. Commented Feb 13, 2020 at 16:51
  • 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. Commented Feb 18, 2020 at 3:08
  • 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
    Commented Feb 21, 2020 at 0:47
18

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.

6
  • 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. Commented Feb 13, 2020 at 17:06
  • @user560822, idiomatically, Java exceptions are for "not normal" situations softwareengineering.stackexchange.com/a/189225 But they can be used in many ways. Commented Feb 14, 2020 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. Commented Feb 14, 2020 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
    Commented Feb 15, 2020 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
    Commented Feb 22, 2020 at 5:29
9

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.

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
    Commented Feb 13, 2020 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
    Commented Feb 13, 2020 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
    Commented Feb 13, 2020 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. Commented Feb 14, 2020 at 20:20
  • @PaulDraper I'm too young
    – chris
    Commented Feb 24, 2020 at 7: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
  • 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
    Commented Feb 15, 2020 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. Commented Feb 15, 2020 at 17:12
3

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.

10
  • Nice analogy ;-) You may want to mention that return values in COM are usually passed with [out] parameters.
    – helb
    Commented Feb 12, 2020 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.
    Commented Feb 12, 2020 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
    Commented Feb 13, 2020 at 2: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. Commented Feb 13, 2020 at 6: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
    Commented Feb 14, 2020 at 19:29
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.

5
  • 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
    Commented Feb 13, 2020 at 0: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. Commented Feb 13, 2020 at 7: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
    Commented Feb 15, 2020 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
    Commented Feb 19, 2020 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
    Commented Feb 20, 2020 at 7: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.

4
  • 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
    Commented Feb 12, 2020 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
    Commented Feb 12, 2020 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. Commented Feb 12, 2020 at 23:56
  • @Clockwork-Muse it may be worth using the words checked and unchecked in your answer
    – Codebling
    Commented Feb 20, 2020 at 7:23
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.

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.

1
  • Go also forces to return tuple with result and error. Rust allows to avoid runtime overhead for Option and Result.
    – gavenkoa
    Commented Jul 4, 2021 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).

Not the answer you're looking for? Browse other questions tagged or ask your own question.