Feature Complete Software
Articles Open Source Contact

Programming with Exceptions

While exceptions are now virtually ubiquitous in programming languages, they are not always used effectively in practice. This is understandable given the fact that popular C++ and Java libraries don't always set a good example.

Early in my programming career I didn't see the value of exceptions. Having worked with C style APIs, I found the C++ try/catch construct to be cumbersome. It seemed like a long winded way to check return codes. Consider:

Example 1 - Handling Exceptions inside a Function

bool log(const char* msg) {
    bool bReturn = true;
    try {
        if (!mFile.isOpen())
            mFile.open(gLogFile);

        mFile.write(msg);
    }
    catch(FileError& e) {
        std::cerr << "Failed to write to log file: " << e.what() << std::endl;
        bReturn = false;
    }
    return bReturn;
}
In a case like this, dealing with the File class's API, and specifically the fact that it throws exceptions, seemed like extra work to me, with no real payoff.

It turns out that to see the payoff, you have to look at the example in a larger context. For example, consider all of the places a log() function is called throughout a small program:

Example 2 - Error Handling via Return Codes

bool subTask1() {
    bool bResult = log("subtask 1");
    if (bResult) {
        bResult = callAPIOne();
        if (!bResult)
            std::cout << "api 1 failed" << std::endl;
        }
    return bResult;
}

bool subTask2() {
    bool bResult = log("subtask 2");
    if (bResult) {
        bResult = callAPITwo();
        if (!bResult)
            std::cout << "api 2 failed" << std::endl;
        }
    return bResult;
}

bool task1() {
    bool bResult = log("task 1");
    if (bResult)
        bResult = subTask1();

    return bResult;
}

bool task2() {
    bool bResult = log("task 2");
    if (bResult)
        bResult = subTask2();

    return bResult;
}

int main() {
    bool bResult = task1();
    if (bResult)
        bResult = task2();

    return bResult ? EXIT_SUCCESS : EXIT_FAILURE;
}
As you can see, there are a lot of lines of code here which are concerned with error handling.
Can changing to exception-based error handling help simplify things?

Example 3 - Using Exceptions instead of Return Codes

void log(const char* msg) {
    if (!mFile.isOpen())
        mFile.open(gLogFile);

    mFile.write(msg);
}

void subTask1() {
    try {
        log("subtask 1");
    }
    catch(exception& e) {
        throw exception("subtask1 failed");
    }

    if (!callAPIOne())
        throw exception("api 1 failed");
}

void subTask2() {
    try {
        log("subtask 2");
    }
    catch(exception& e) {
        throw exception("subtask2 failed");
    }

    if (!callAPITwo())
        throw exception("api 2 failed");
}

void task1() {
   try {
       log("task 1");
       subTask1();
   }
   catch(exception& e) {
       throw exception("task 1 failed");
   }
}

void task2() {
   try {
       log("task 2");
       subTask2();
   }
   catch(exception& e) {
       throw exception("task 2 failed");
   }
}

int main() {
    int result = EXIT_SUCCESS;
    try {
        task1();
        task2();
    }
    catch(exception& e) {
        std::cerr << "program failed: " << e.what() << std::endl;
        result = EXIT_FAILIURE;
    }
    return result;
}
At this point exceptions don't seem to have helped much, if at all. It seems we have just traded one error handling syntax for another. Line count has gone from 56 to 63.

Now comes the key observation, however: with exceptions, we don't need to have error handling at every level:

Example 4 - Using Exceptions with Minimal Catch Blocks

void log(const char* msg) {
    if (!mFile.isOpen())
        mFile.open(gLogFile);

    mFile.write(msg);
}

void subTask1() {
    log("subtask 1");
    if (!callAPIOne())
        throw exception("api 1 failed");
}

void subTask2() {
    log("subtask 2");
    if (!callAPITwo())
        throw exception("api 2 failed");
}

void task1() {
   log("task 1");
   subTask1();
}

void task2() {
   log("task 2");
   subTask2();
}

int main() {
    int result = EXIT_SUCCESS;
    try {
       task1();
       task2();
    }
    catch(exception& e) {
       std::cerr << "program failed: " << e.what() << std::endl;
       result = EXIT_FAILIURE;
    }
    return result;
}
Now the benefit of exceptions becomes apparent. Line count is just 41, down from 56 in Example 2 and 63 in example 3. There is much less code here dealing with error handling, yet we still have assured that every error will be handled. Of course we also have the option of selectively adding more try/catch blocks, in cases where we need to handle errors more locally.

Sources of Misunderstanding

I presented these examples in C++, yet I have also seen Java and Python code which does error handling done in the style of Example 2 or Example 3. Programmers tend to mimick coding styles they have been exposed to through libraries, and no doubt many programmers have been influenced by working with C APIs, which of course use the style of Example 2. Since the C language lacks exceptions, it follows that errors must be handled or propogated at every level. Example 3 is a demonstration of using Exceptions in the style of return codes.

Unfortunately it seems that some Java language and library designers fell into the trap of treating exceptions as if they are error codes as well. In fact I think this has been a major source of misunderstanding for programmers. Java provides "checked" exceptions, which actually require that they are accounted for at each level. There are a couple of ways around this - functions can declare "throws Exception", or APIs can be written to throw "unchecked" exceptions - but unfortunately the Java libraries themselves are littered with examples of checked exceptions, with error checking at every level.

Conclusion

Andrew Koenig has said that abstraction is "selective ignorance." I think that choosing to minimize the use of catch blocks is a good example of such selective ignorance.

If you follow good coding practices with regards to separation of responsibilities, you will find that your code has many levels of function calls. There is very often little to gain by including error handling code inside intermediate functions, and in fact doing so without being careful can result not only in a loss of clarity but in a loss of valuable error details. (In fact, take another look at Example 3 above - there are several places where error detail is "swallowed", to demonstrate the point).

So the guideline I suggest for exceptions is to use catch blocks only when necessary. It maximizes the readibility of the code, keeping it focused on the task at hand, instead of on those cases where something has gone wrong (i.e. the "exceptional" cases!).

Note that while the issues I have discussed here apply generally to most any language which supports exceptions, there are some specific issues to be aware of when working with exceptions in C++. I will discuss C++ exception specifics in a future article.

Posted by Steve Hutton on Thu, 23 Nov 2006 05:39:00 +1600 [Permalink] [More on C++]

Previous: Python Web Frameworks

Related Links:

Bruce Eckel on Checked Exceptions


Post a comment

Your Name:

Comment: