Why should exceptions be used conservatively?

I often see/hear people say that exceptions should only be used rarely, but never explain why. While that may be true, rationale is normally a glib: "it's called an exception for a reason" which, to me, seems to be the sort of explanation that should never be accepted by a respectable programmer/engineer.

There is a range of problems that an exception can be used to solve. Why is it unwise to use them for control flow? What is the philosophy behind being exceptionally conservative with how they are used? Semantics? Performance? Complexity? Aesthetics? Convention?

I've seen some analysis on performance before, but at a level that would be relevant to some systems and irrelevant to others.

Again, I don't necessarily disagree that they should be saved for special circumstances, but I'm wondering what the consensus rationale is (if such a thing exists).


Solution 1:

The primary point of friction is semantics. Many developers abuse exceptions and throw them at every opportunity. The idea is to use exception for somewhat exceptional situation. For example, wrong user input does not count as an exception because you expect this to happen and ready for that. But if you tried to create a file and there was not enough space on disk, then yes, this is a definite exception.

One other issue is that exceptions are often thrown and swallowed. Developers use this technique to simply "silence" the program and let it run as long as possible until completely collapsing. This is very wrong. If you don't process exceptions, if you don't react appropriately by freeing some resources, if you don't log the exception occurrence or at least not notify the user, then you're not using exception for what they are meant.

Answering directly your question. Exceptions should rarely be used because exceptional situations are rare and exceptions are expensive.

Rare, because you don't expect your program crash at every button press or at every malformed user input. Say, database may suddenly not be accessible, there may not be enough space on disk, some third party service you depend on is offline, this all can happen, but quite rarely, these would be clear exceptional cases.

Expensive, because throwing an exception will interrupt the normal program flow. The runtime will unwind the stack until it finds an appropriate exception handler that can handle the exception. It will also gather the call information all along the way to be passed to the exception object the handler will receive. It all has costs.

This is not to say that there can be no exception to using exceptions (smile). Sometimes it can simplify the code structure if you throw an exception instead of forwarding return codes via many layers. As a simple rule, if you expect some method to be called often and discover some "exceptional" situation half the time then it is better to find another solution. If however you expect normal flow of operation most of the time while this "exceptional" situation can only emerge in some rare circumstances, then it is just fine to throw an exception.

@Comments: Exception can definitely be used in some less-exceptional situations if that could make your code simpler and easier. This option is open but I'd say it comes quite rare in practice.

Why is it unwise to use them for control flow?

Because exceptions disrupt normal "control flow". You raise an exception and normal execution of the program is abandoned potentially leaving objects in inconsistent state and some open resources unfreed. Sure, C# has the using statement which will make sure the object will be disposed even if an exception is thrown from the using body. But let us abstract for the moment from the language. Suppose the framework won't dispose objects for you. You do it manually. You have some system for how to request and free resources and memory. You have agreement system-wide who is responsible for freeing objects and resources in what situations. You have rules how to deal with external libraries. It works great if the program follows the normal operation flow. But suddenly in the middle of execution you throw an exception. Half of the resources are left unfreed. Half have not been requested yet. If the operation was meant to be transactional now it is broken. Your rules for handling resources will not work because those code parts responsible for freeing resources simply won't execute. If anybody else wanted to use those resources they may find them in inconsistent state and crash as well because they could not predict this particular situation.

Say, you wanted a method M() call method N() to do some work and arrange for some resource then return it back to M() which will use it and then dispose it. Fine. Now something goes wrong in N() and it throws an exception you didn't expect in M() so the exception bubbles to the top until it maybe gets caught in some method C() which will have no idea what was happening deep down in N() and whether and how to free some resources.

With throwing exceptions you create a way to bring your program into many new unpredictable intermediate states which are hard to prognose, understand and deal with. It's somewhat similar to using GOTO. It is very hard to design a program that can randomly jump its execution from one location to the other. It will also be hard to maintain and debug it. When the program grows in complexity, you just going to lose an overview of what when and where is happening less to fix it.

Solution 2:

While "throw exceptions in exceptional cirumstances" is the glib answer, you can actually define what those circumstances are: when preconditions are satisfied, but postconditions cannot be satisfied. This allows you to write stricter, tighter, and more useful postconditions without sacrificing error-handling; otherwise, without exceptions, you have to change the postcondition to allow for every possible error state.

  • Preconditions must be true before calling a function.
  • Postcondition is what the function guarantees after it returns.
  • Exception safety states how exceptions affect the internal consistency of a function or data structure, and often deal with behavior passed in from outside (e.g. functor, ctor of a template parameter, etc.).

Constructors

There's very little you can say about every constructor for every class that could possibly be written in C++, but there are a few things. Chief among them is that constructed objects (i.e. for which the constructor succeeded by returning) will be destructed. You cannot modify this postcondition because the language assumes it is true, and will call destructors automatically. (Technically you can accept the possibility of undefined behavior for which the language makes no guarantees about anything, but that is probably better covered elsewhere.)

The only alternative to throwing an exception when a constructor cannot succeed is to modify the basic definition of the class (the "class invariant") to allow valid "null" or zombie states and thus allow the constructor to "succeed" by constructing a zombie.

Zombie example

An example of this zombie modification is std::ifstream, and you must always check its state before you can use it. Because std::string, for example, doesn't, you are always guaranteed that you can use it immediately after construction. Imagine if you had to write code such as this example, and if you forgot to check for the zombie state, you'd either silently get incorrect results or corrupt other parts of your program:

string s = "abc";
if (s.memory_allocation_succeeded()) {
  do_something_with(s); // etc.
}

Even naming that method is a good example of how you must modify the class' invariant and interface for a situation string can neither predict nor handle itself.

Validating input example

Let's address a common example: validating user input. Just because we want to allow for failed input doesn't mean the parsing function needs to include that in its postcondition. It does mean our handler needs to check if the parser fails, however.

// boost::lexical_cast<int>() is the parsing function here
void show_square() {
  using namespace std;
  assert(cin); // precondition for show_square()
  cout << "Enter a number: ";
  string line;
  if (!getline(cin, line)) { // EOF on cin
    // error handling omitted, that EOF will not be reached is considered
    // part of the precondition for this function for the sake of example
    //
    // note: the below Python version throws an EOFError from raw_input
    //  in this case, and handling this situation is the only difference
    //  between the two
  }
  int n;
  try {
    n = boost::lexical_cast<int>(line);
    // lexical_cast returns an int
    // if line == "abc", it obviously cannot meet that postcondition
  }
  catch (boost::bad_lexical_cast&) {
    cout << "I can't do that, Dave.\n";
    return;
  }
  cout << n * n << '\n';
}

Unfortunately, this shows two examples of how C++'s scoping requires you to break RAII/SBRM. An example in Python which doesn't have that problem and shows something I wish C++ had – try-else:

# int() is the parsing "function" here
def show_square():
  line = raw_input("Enter a number: ") # same precondition as above
  # however, here raw_input will throw an exception instead of us
  # using assert
  try:
    n = int(line)
  except ValueError:
    print "I can't do that, Dave."
  else:
    print n * n

Preconditions

Preconditions don't strictly have to be checked – violating one always indicates a logic failure, and they are the caller's responsibility – but if you do check them, then throwing an exception is appropriate. (In some cases it's more appropriate to return garbage or crash the program; though those actions can be horribly wrong in other contexts. How to best handle undefined behavior is another topic.)

In particular, contrast the std::logic_error and std::runtime_error branches of the stdlib exception hierarchy. The former is often used for precondition violations, while the latter is more suited for postcondition violations.

Solution 3:

  1. Expensive
    kernel calls (or other system API invocations) to manage kernel (system) signal interfaces
  2. Hard to analyze
    Many of the problems of the goto statement apply to exceptions. They jump over potentially large amounts of code often in multiple routines and source files. This is not always apparent from reading the intermediate source code. (It is in Java.)
  3. Not always anticipated by intermediate code
    The code that gets jumped over may or may not have been written with the possibility of an exception exit in mind. If originally so written, it may not have been maintained with that in mind. Think: memory leaks, file descriptor leaks, socket leaks, who knows?
  4. Maintenance complications
    It's harder to maintain code that jumps around processing exceptions.