In my previous article I looked at a basic reason why exceptions are necessary. In retrospect it was more of a look at why simple error codes are insufficient; it isn’t clear that C++/Java style exceptions are the correct solution either. I do suspect some kind of exception-like mechanism will be required. With that in mind we should look at the problems exceptions have. That is, despite being necessary, why is it that they aren’t universally accepted.
Here I’ll present a list the major criticisms of exceptions. I won’t go too deeply in to each topic but will try to give an overview of the problem. There is obviously an endless list of complaints about exceptions, but mostly they can be grouped into the items here.
Performance / Efficiency
Performance is probably the most controversial topic about exceptions. While the concern is legitimate at times, the issue is complicated by a lot of misconceptions and bad information. The first issue is with the memory requirements of exceptions. Whether it is a simple descriptive string, or a full stack trace, there is data overhead to the exception. This isn’t a requirement of exceptions however. Consider that in C++ you can throw simple integer types if you really wanted. Alas, many languages don’t offer lean exceptions and you’re stuck with what you get.
Execution time of exceptions wildly varies depending on language and compiler. In a fair comparison you always have to consider two different timings: the non-exception code flow and the exception code flow. The former refers to the time it takes the code to execute when an exception doesn’t happen, and thus the latter is the cost of propagating an exception itself. In a return code model these times are combined together as you must always deal with the return codes. The cost is the same whether there is an error or not.
It should be noted here that exceptions can be implemented with zero cost in the non-error case — and some C++ compilers do this. The cost comes at throw time and involves table lookups. Should you be writing a hard real-time program, the added memory of these lookup tables might require investigation.
Unlike simple integer error codes there is no agreed definition of what an exception actually is. Each language has a slightly different take on it. Exceptions are not compatible between languages, and often not even between different compilers of the same language. This means that on the boundaries to your code, like library functions, exceptions are often forbidden. You have to revert to simple error codes at this point.
Perhaps this wouldn’t be tragic, except languages like C++ will allow you to throw an exception beyond the library boundary resulting in undefined behaviour. For each library function you wish to expose you must catch the exceptions manually and translate into a suitable error code. Calling a library poses a similar problem, but not as drastic. You’ll get simple return codes back but then must convert them to exceptions on your own. Such work could be handled by the language itself.
The boundary problem happens also when using the network, shared memory, or some other protocol. Simple error codes are trivial to serialize in both directions. Exceptions however require a bit of work, and generally involve the loss of information on the receiving side. While this loss of information is not tragic, it does reduce the value of using exceptions. A well designed framework could avoid all issues here, alas I have yet to see such an exception framework.
Not an isolated feature
Exceptions can’t exist as a feature on their own. To be used effectively there must be several other supporting language features. Without these features writing exception-safe code becomes burdensome, if not dangerous.
One of two critical features that needs to be offered is a “defer” mechanism or an “RAII” pattern. The defer mechanism is simply a way to say some code should be run once the current scope exists. RAII encapsulates such behaviour on a resource such that when the scope exits cleanup code can also be called. RAII is really nothing more than guaranteeing immediate object cleanup — many object based languages offer this, though not explicitly.
A “finally” block is also very helpful, though not essential. Going even further, “finally” is not sufficient to adequately support exceptions: you need one of defer and RAII as well. This is actually a significant problem with Java. While you certainly can use finally to write exception safe code, it suffers from the bulky syntax problems and being far removed from the initialization code it is cleaning.
Nothing is inherently bad with theses features. They do however increase the complexity of the language. Proper use of exceptions requires learning these additional features. A language with exceptions which fails to offer these features is deficient.
When C++ added exceptions it included a “try” block as a way to catch exceptions. This perhaps seemed like a good idea at the time, since you’d like to indicate where you might be catching an exception. Now however it seems superfluous: any part of the code can throw an exception, and you might handle them anywhere, so why should you have to declare “try”, it should be implicit.
This leads to the somewhat bulky syntax of dealing with exceptions. The “catch” expression itself isn’t so large, but you can’t use it without a “try” and without full bracketed code. In a large block of code the overhead may not seem like much, but if you’re trying to handle an exception for a single function the overhead is very large. In this case your code becomes significantly more bloated than if you had used return codes.
It’s all or nothing / Legacy Code
A major force of resistance against exceptions is legacy code. Exceptions cannot be propagated through any code which is not exception safe. The use of exceptions thus implies that all code in the project must be exception safe. Any code which existed prior to this point would have to be scoured and modified to work with exceptions. In many cases this exception upgrading is as costly as rewriting the code — something few projects can afford to do.
You can certainly make new modules of your projects exception safe. You could even segregate the old code into libraries and treat it as such. You’ve not solved the issue however, you’ve just converted it to the library boundary issue. Though even if you don’t use exceptions, the tools, like RAII, finally blocks, and deferred, still offer a lot of utility.
Could a language, or compiler, feature ease this burden? Perhaps. As it could ease the burden of library boundaries it could also offer a similar feature within a module.
Too Many Exceptions
Not all functions can directly deal with all types of errors. The catch mechanism allows functions to deal only with those errors of interest to them. This is a good idea in theory, but in practice something went wrong. It only works if the number of exceptions is very low and they are well classified.
Unfortunately it seems that every library has defined its own set of exceptions, and every author has a different notion of how they should be structured. Documentation is also lacking in many places and it becomes difficult, if not impossible, to determine what an API can even throw. You end up being forced to catch all exceptions, defeating a key part of the typed catch system.
This is not a necessary problem with exceptions. A better exception hierarchy can be made, but it requires good forsight and can’t really be fixed after the fact.
The Checked Exception Mess
Along with exceptions came the idea of checked and unchecked exceptions. Some exceptions could be thrown implicitly while others had to be explicitly handled. These checked exceptions become part of the function signature and are rigidly enforced by the compiler. It seemed like a great idea for strict error handling. The problems with this approach are numerous however: at a minimum conflicting with some of the basic notions of object oriented programming and abstraction.
This is exemplified in Java where no program escapes the need to have a catch-all clause and simply rethrow them as a different exception type. This type of code brings no added value and just results in a lot of useless code. These wrappers further neuter the value of a type exception system since the true types are completely hidden.
Static Exceptions / Exceptions in Exceptions
While exceptions during the main program path can be readily handled, there is some problem when it comes to global variables, statics, and other startup initialization. To be fair however, this problem is not new to exceptions. Errors during initialization are a problem regardless of what mechanism is used. Though it is highly dissatisfying that a language would offer static initialization yet have no way to handle such errors.
Perhaps a related issue is dealing with exceptions which are thrown while already in an exception. C++ thought it was a good idea to simply call terminate. Again, dealing with an error in an error is not easy in any system, but one should think the behaviour is at least safely defined. In fact, since errors within errors are not infrequent, there should be a well defined and clear mechanism to deal with them.
Lacking proper support for static initialization and errors within errors leaves exceptions feeling incomplete.
An oft repeated argument I’ve seen against exceptions is that they are naught but glorified gotos. The label is perhaps misleading; the intent is to indicate it bypasses the strict imperative flow. Any statement in a program now has both a normal sequence (to the next statement) and an exception sequence which brings it somewhere else. For many people this alternate flow is considered harmful.
I’ve never seen this argument presented strongly enough to consider it a fully valid issue. Simple things like “break” and “continue” keywords can also alter flow. Likewise can a mid-function “return” also is a kind of “goto”. New language features like a “defer” statement is certainly a non-standard flow, as are destructors, or even the “finalize” handlers used in garbage collection. Even languages without exceptions have these alternate program flows.