Exceptions Are Evil

Exceptions are evil.

Now that I’ve pissed everyone off, I can get on with explaining. Rather than talk in the abstract about this in the abstract, which is not helpful at all, I am going toss in an example or two. It is far easier to illustrate my point with examples in this case.

First, let me start by saying that the general concept of exceptions is not necessarily evil. Indeed, any program that is properly written must deal with exceptions in some manner, preferably without crashing randomly. Where things tend to go horribly wrong is when a programming language implements support for some flavour of exceptions. For instance, C++. It can obviously also go horribly wrong when a programmer messes up in a language that doesn’t implement any version of exceptions directly.

Leaving aside the syntactic problems with exceptions for the moment, let us consider what an exception actually is. The word would suggest an exception should be reserved for a truly unusual circumstance – something that should not normally happen. But what exactly does “unusual” mean?

Let’s examine the case of allocating memory on the heap. Ordinarily, this succeeds without a lot of fuss. But what happens if it fails for some reason, say there is no more memory available or the process’s address space is full? In C, the malloc() family functions return NULL to indicate this condition. The program then has to check the return value or simply crash when dereferencing the resulting NULL pointer. In most programs, running out of memory would be an exceptional condition so it would seem to be an excellent candidate for exceptions.

Let’s examine another case. Suppose you are opening a file to read some stuff in it. Now suppose access is denied or the file doesn’t exist. These are exceptions, too, right? Well, not necessarily. If you’re iterating through a list of possible file names to find a file that does exist, or which directory it exists in, experiencing a file not found situation is hardly an unusual circumstance, is it? Having a file with no access permission is likewise fairly normal on systems that support such things. If the file in question happens to be a critical part of the overall program that is running, that might constitute an exception since that is an unusual circumstance meaning that the program is incorrectly installed. But a missing input file to a compiler is likely not an exceptional condition and is one that the compiler will want to deal with directly.

On the surface the two situations above look the same, but they really aren’t. With the second example, the situations described are error conditions, but they are not unusual conditions. They are, in fact, ordinary conditions that are expected to occur in the ordinary operation of the system. They are, by definition, not exceptional.

One of the biggest issues I have with exceptions as they are implemented and used in modern systems is that the notion of an exception has been abused into a general purpose system for returning any error condition, routine or otherwise. I can possibly defend an exception on memory allocation failure, but I have a great deal of trouble accepting that an exception is the right solution when a file simply does not exist.

Modern systems have conflated error handling and exception handling. This may be partly due to the fact that without an exception mechanism, exceptions and ordinary errors have to be handled the same way – by an error indication from a particular call. The other source of the conflation is possibly due to the fact that many “software architects” do not understand why everything is not an exception.

All of that would be relatively minor, however, if the syntax for exceptions were not horribly verbose and insanely tedious to use, even when the language itself provides a solid bug-free implementation. The biggest problem is that this excess verbosity and extra boilerplate code encourages moving the error handling to a point distant from the actual source of the problem. The idea is that the ordinary flow of the code is easy to discover by extracting the error handling from within the main flow. However, this makes the flow non-obvious in the case of something like a missing file. If you do have a solution to a missing file, say by trying a different file, you can’t just continue where you left off if you have one big block and handle all the exceptions at once. Instead, you have to wrap everything that can fail with its own exception handling boilerplate. Now you have duplicated the exact thing you thought you could avoid by using exceptions in the first place! For instance:

result = somefunc();
if (result < 0)
{
    result = tryotherfunc();
    if (result < 0)
    {
        handle_error();
    }
}

if (do_something_with_result(result) < 0)
{
    handle_error();
}

if (do_something_else_with_result(result) < 0)
{
    handle_error();
}

The above is relatively clear. The precise mechanics of how handle_error() would have to work depends on the specific calls, of course. Let’s see what that might look like if we have a language using a “try { code… } catch ….” structure:

try
{
    try
    {
        result = somefunc();
    }
    catch (exception)
    {
        result = tryotherfunc();
    }
    
    do_something_with_result(result);
    do_something_else_with_result(result);
}
catch (exception1)
{
    handle_error();
}

That doesn’t look too bad, does it? But what if we need to do something different depending on which failure condition occurred with, say, do_something_with_result()? And suppose that do_something_else_with_result() might also throw the same exception for a different reason and we can handle that one too? Oops, now we need a try/catch block around each call. We can no longer get away with just the outer one. And for each exception we need to handle, we have to add another catch statement after the try block. It starts to get confusing rapidly.

Of course, you have to add some sort of if/else structure to do the same thing in the first example, too, but that is not going to be any more verbose than the try/catch structure.

There is another case here that should be examined, too. Let’s look at the case where you want to remove a file. Now suppose we are using the first style. If an error occurs, the call returns an indicator of such. If we are using the second style with exceptions, it throws an exception. Now suppose you don’t care if the file didn’t already exist. In the former, you can just ignore the condition. In the latter, you have to explicitly trap the exception and ignore it. The same situation often occurs when closing a file before exiting a program. Sure, errors can occur at that point, but what can you actually do about them? If exceptions are being thrown in that case, the do no good but you would still have to handle them or get mysterious unhandled exceptions at program shutdown.

There is another case where exceptions seem to be the best solution. Suppose the call is to some sort of implicit function, say an object constructor or an overloaded operator (overloading is evil too, by the way). In many languages, this is needed because objects can be constructed implicitly. That means there is no code flow that can trap an error return. The same thing applies to a destructor, which is often called implicitly. It is my contention, however, that if a constructor needs to do something that might fail, (resource acquisition, for instance), it really should not be doing anything implicitly. Instead, some sort of factory operation should be used to obtain a new object. In that case, explicit destruction is then obviously indicated. In short, if an object cannot be implicitly instantiated, there is no need for exceptions. Similarly, if you eliminate the notion of operator overloading, you eliminate the possibility of needing to fail randomly in expressions, too.

I should note that explicit construction is not actually horribly verbose in most cases, and certainly not terribly more verbose than what would be needed to express the static initializer anyway. Also, being explicit with an operation on an object does not preclude being able to chain operations together. It’s easy enough to put an object into an error state that can propagate through chained calculations and then be checked at the end. Sure, you might need multiple statements to chain the calculation together with explicit destruction being required and that might be a bit tedious, but it has the advantage of being absolutely clear what is going on.

And there is the final disadvantage of using exceptions instead of explicit instantiation and destruction, though this is not specific to exceptions per se. It is not at all clear whether a particular usage pattern is doing something dumb or not if you can just chain stuff together with implicit construction and destruction. Sure, you can create resource leaks with explicit destruction, but at least you can see if you are creating and destroying stacks of objects needlessly, and you can bail out in a controlled manner at any stage you like. It is also explicit that something which might fail is happening so you generally have a clear idea what failed if your program crashes at a particular location.

To this point, I haven’t even examined how the actual language exception implementation could be problematic, either. Suppose you have a language with explicit memory management like C++. Now suppose exceptions are objects. You throw an exception, but you have to dynamically allocate it somehow because it would go out of scope as the stack unwinds otherwise. How do you ensure the freshly minted exception gets freed correctly after the exception is handled? What if the object was a static object instead of a dynamic one (which also wouldn’t go out of scope)? What if memory allocation fails when creating the new exception object? Yowza! Things can go wrong in a hurry! Of course, in a language with automatic memory management, like javascript or java, this is much less of a problem. (Though what do you do if the exception object fails to initialize? Throw an exception?)

In short, in my never humble opinion exceptions are useless syntactic HFCS (high fructose corn syrup). They taste great but have at best dubious benefits and more likely are deleterious to maintainability or correctness in the long run. Even when well implemented using something less verbose than try/catch, they do not seem to be a substantial improvement over classical error handling. And, if they are only used for truly exceptional circumstances, the classical error handling would still need to be present for many circumstances (non-exceptional failures).

There is nothing wrong with explicit error handling inline in cleanly structured code. If you think you need exceptions to make your code clear, you are going down the wrong path.

Leave a Reply

Your email address will not be published. Required fields are marked *