Question

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?

Was it helpful?

Solution

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.

OTHER TIPS

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.

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.

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.

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.

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.

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.

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.

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.

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.

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

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.

Licensed under: CC-BY-SA with attribution
scroll top