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

