Wednesday, May 19, 2004


Return-codes vs. Exceptions, Part 129

User Interface Design for ProgrammersThere's been an ongoing debate in the software development world regarding whether return-codes or exceptions should be used. Here's a brief recap.

Joel lists several good reasons why return-codes are preferred over exceptions:

#1 "They (exceptions) are invisible in the source code" - thus, they are difficult to maintain ("...even careful code inspection doesn't reveal potential bugs")

#2 "They create too many possible exit points for a function" - effective cleanup code is predicated upon predictable unwinding of state as a method or function terminates

Joel continues, "...I consider exceptions to be no better than "goto's", considered harmful since the 1960s, in that they create an abrupt jump from one point of code to another. In fact they are significantly worse than goto's..."

Sergio states (regarding Joel), "His stance against exceptions as a method for handling abnormal program behaviour is just plain wrong. Joel recommends using error return codes, and dealing with them immediatly. There are two big problems with this approach:

It places abnormal behaviour treatment inline with normal execution. It hurts code readability.

Without exceptions, dealing with code transactions and rollbacks produces an imense tangling of ifs or procedure calls.

There is a valid discussion open on whether exceptions should be checked or unchecked, but the mechanism itself is, for me, proven..."

Ned agrees with Sergio, "Exceptions keep the code clean". At least he provides some examples.

And, in a recent interview, James Gosling had some interesting things to say. He wasn't speaking directly about exceptions versus return-codes, but he had some salient points that relate to the mission-critical software world.

"You talk to people in banks, where large quantities of money get lost if there's a problem. They take failure very seriously. The spookiest folks are the people who have been doing real time software for a long time. I've spent a certain amount of time with that crowd. By and large these folks are very conservative and very careful. A lot of them know what it's like to go visit the family of the deceased and explain the bug to them. There are a number of places that have policies that if a test pilot augers in, then once it's all figured out what happened, the people who engineered the thing that failed have to go explain it to the family. I've actually only met one person who has ever actually had to do that. That would change your attitude about dealing with failure really quickly"


Here's my take: Joel is dead-on. I'll tackle Sergio's item number one first. Look: we (the software development community) have a problem with software quality. I don't think I'll get much disagreement on this fact. Software quality, in general, sucks. The reason for this is that many developers are too lazy to instrument, monitor and and respond to all sorts of strange conditions.

In other words, many of us are undisciplined. We're more worried about "readability" (and I disagree with that contention as well - but I'll get to that) than whether or not or software will kill anyone, debit the wrong account by a million bucks, or screw up the actuarial table for 83 year-old transvestites.

Like it or not, dealing with aberrant conditions is a contract we agree to when we decide to be professional and responsible software developers. Treating "abnormal behaviour inline with normal execution" is a misstatement. We need to deal with the unexpected. And the best place to do it is the place where you can "unwind" the logic that's gone bad.

My guess is that Sergio never wrote a database engine or other mission-critical, system-level code. This is the kind of logic you'd implement (it's a simplified example only, so please don't send me syntax error, faulty logic, or "you coulda used diagnostic macros" messages):



do { try {

if ((rc = tableCredits.open()) != OK) {
// log
break;
}
bUnwindTableCreditsOpen = TRUE;

if ((rc = tableCredits.lock()) != OK) {
// log
break;
}
bUnwindTableCreditsLock = TRUE;

if ((rc = tableDebits.open()) != OK) {
// log
break;
}
bUnwindTableDebitsOpen = TRUE;

if ((rc = tableDebits.lock()) != OK) {
// log
break;
}
bUnwindowTableDebitsLock = TRUE;

if ((rc = ::IntegralTransaction(tableCredits, tableDebits, curAmount)) != 0) {
// log
break;
}

} catch (...) {
// catch miscellaneous exceptions here
} } while (0);

if (bUnwindTableDebitsUnlock) {
tableDebits.unlock();
}
if (bUnwindTableDebitsOpen) {
tableDebits.close();
}
if (bUnwindTableCreditsLock) {
tableCredits.unlock();
}
if (bUnwindTableCreditsOpen) {
tableCredits.close();
}
return (rc);


Can you imagine trying to unwind these kinds of "abnormal events" outside the scope of the method that was orchestrating this sort of activity? It wouldn't be fun.

And my take is that readability isn't compromised here. No one is espousing tons of nested if's or other convoluted logic to keep track of state. Do it the easy way. If you have to throw an exception, do it. Right to the bottom of your method. Or, if you can't, hit the "break" (pun intended). Either way, you can start unwinding based upon your current state without compromising the integrity of an 84 year-old transvestite's term-life premium.

Trust no one. Check everything. Log everything. Go forth and prosper.

No comments: