« VMWare Workstation autostart vmware-user | Main | Noodler Black Inks »

Exception Handling

If you google “Exceptions Considered Harmful”, you’ll find several folks who have a bone to pick with exceptions. The best arguments (in my opinion) are these:

  1. Exception handling introduces a hidden, “out-of-band” control-flow possibility at essentially every line of code. Such a hidden control transfer possibility is all too easy for programmers to overlook – even experts. When such an oversight occurs, and an exception is then thrown, program state can quickly become corrupt, inconsistent and/or difficult to predict (think about an exception unexpectedly being thrown part way through modifying a large data structure, for example). (Jason Robert Carey Peterson)
  2. Exception handling does not fit well with most of the highly parallel programming models currently in use or being explored (fork/join, thread pools and task queues, the CSP/actor model etc), because exception handling essentially advocates a kind of single-threaded “rollback” approach to error handling, where the path of execution – implicitly a single path – is traversed in reverse by unwinding the call stack to find the appropriate error handling code. (Jason Robert Carey Peterson)
  3. Exceptions create hard-to-debug code. Every marginally important error condition in code that relies on exceptions is, and in fact has to be, treated as a potentially fatal error. This creates a situation that can be dubbed “exception spam”, which is especially problematic when code is reused across multiple contexts, and the severity assumptions of certain errors are not true in all contexts. Exceptions just keep coming (and being handled) on all sorts of seemingly innocent occasions. The problem is, once the “break on exceptions” debugger functionality is off the table due to exception spam, you are relegated to manual code analysis to find where things are breaking and why. For example, consider the standard-looking code snippet:
    std::ifstream file(“accounts.db”);
    Normally, errors are handled via an if (!file.exists()) conditional afterward. But if ifstream were to throw an exception on error, there would possibly be no if clause afterwards to check for that exception because it may well be handled a few call frames above, by the client code at some distant level of abstraction. You can’t set a breakpoint on a line of code that doesn’t exist, which means it becomes extremely difficult to track down where that exception came from. In order to figure out what’s going on, you have to disable the “break on exceptions” feature of your debugger, and go find the bug the old-fashioned way OR wrap every function call in its own unique try/catch block. (Dennis Gurzhii)
  4. Exceptions are invisible in the source code. Looking at a block of code, including functions which may or may not throw exceptions, there is no way to see which exceptions might be thrown and from where. This means that even careful code inspection doesn’t reveal potential bugs. (Joel on Software)
  5. Exceptions create too many possible exit points for a function. To write correct code, you really have to think about every possible path through your function. Every time you call a function that can raise an exception (a fact which may be hard to know, per the previous point) and don’t catch it on the spot, you create opportunities for surprise bugs caused by functions that terminated abruptly, leaving data in an inconsistent state (think data structures), or other code paths you didn’t think about. (Joel on Software)

But if not exceptions, then what? To quote Joel on Software (a really smart fellow), back in 2003:

A better alternative is to have your functions return error values when things go wrong, and to deal with these explicitly, no matter how verbose it might be. It is true that what should be a simple 3 line program often blossoms to 48 lines when you put in good error checking, but that’s life, and papering it over with exceptions does not make your program more robust. I think the reason programmers in C/C++/Java style languages have been attracted to exceptions is simply because the syntax does not have a concise way to call a function that returns multiple values, so it’s hard to write a function that either produces a return value or returns an error. (The only languages I have used extensively that do let you return multiple values nicely are ML and Haskell.) In C/C++/Java style languages one way you can handle errors is to use the real return value for a result status, and if you have anything you want to return, use an OUT parameter to do that. This has the unfortunate side effect of making it impossible to nest function calls, so result = f(g(x)) must become:

T tmp;
if (ERROR  g(x,tmp))
    errorhandling;
if (ERROR  f(tmp, result))
    errorhandling;

This is ugly and annoying but it’s better than getting magic unexpected gotos sprinkled throughout your code at unpredictable places.

It’s important to recognize and understand that the error-code methodology does have drawbacks! The error codes don’t contain much information (what was the filename you couldn’t read?), and (as Joel points out) checking them all the time is ugly and annoying. But that’s what good programming means: checking for, and handling, errors! Doing the same thing with exceptions doesn’t make the need go away, just less ugly with a harder to follow error-handling path. Take for example, the following code:

class Foo {
public:
    Foo();
    ~Foo();
private:
    Bar *a;
    Baz *b;
};	

Foo::Foo() :
  a(new Bar),
  b(new Baz)
{}

Foo::~Foo()
{
    delete a;
    delete b;
}

Can you see the memory leak? If new Baz throws an exception, the destructor for Bar is never invoked, so a is leaked. The alternative, proposed by exception advocates and C++ experts (namely, Fabrizio Oddone) is:

Foo::Foo() :
  a(NULL),
  b(NULL)
{
    std::auto_ptr<Bar> exceptionSafeBar(new Bar);
    std::auto_ptr<Baz> exceptionSafeBaz(new Baz);
    a = exceptionSafeBar.release();
    b = exceptionSafeBaz.release();
}

If the Baz allocation fails, the auto_ptr destructor WILL be called (you knew that, right? it’s because while Bar didn’t go out of scope, exceptionSafeBar does go out of scope as the result of an exception), which will call the Bar destructor. Then you can “release” those pointers, which is a non-throwing operation (can you tell by looking at them that they cannot throw an exception?). Nice and clean, right?

An alternative proposed by C programmers would be this:

struct Foo *f = malloc(sizeof(struct Foo));
if (f == NULL) {
    return NULL;
}
f.a = malloc(sizeof(struct Bar));
f.b = malloc(sizeof(struct Baz));
if (!f.a || !f.b) {
    if (f.a) free(f.a);
    if (f.b) free(f.b);
    free(f);
    return NULL;
}

The one with exceptions is a lot fewer lines of code, but which one do you find easier to understand? Which would be easier to debug? Which would you have thought of?

But that’s just memory allocation – and the proposed exception-friendly solution is, essentially, a form of garbage collection. And what about complex data structures, or some other thing where a potential failure can come part-way through a change? The basic C++ answer to this is the same: hide the cleanup in destructors of custom error-handling classes that are created on a per-operation basis. It’s like a dead-man switch: successful execution has to disarm the bomb (er, I mean, “disarm the clean-up variables”) whose purpose is to destroy everything in the case of unexpected death (er, “an exception”). That’s what the call to release() did, among other things. Now ask yourself: what happens if a destructor encounters an exception? How familiar are you with the rules governing destructor ordering in exceptional cases and how to work around it when necessary? How much implicit behavior do you want to rely on for your error-handling?

The strength of exception handling is also its greatest weakness: the fact that it’s hidden. The big benefit is that your “happy path” through the code is clean and obvious. The big downside is that the error paths (both the sources of errors and the handling of errors) are largely invisible. (And that’s in addition to the challenges when doing threaded code.)

TrackBack

TrackBack URL for this entry:
https://www.we-be-smart.org/mt/mt-tb.cgi/790

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)

About

This page contains a single entry from the blog posted on November 4, 2015 9:58 AM.

The previous post in this blog was VMWare Workstation autostart vmware-user.

The next post in this blog is Noodler Black Inks.

Many more can be found on the main index page or by looking through the archives.

Creative Commons License
This weblog is licensed under a Creative Commons License.
Powered by
Movable Type 3.34