The most enduringly popular post on this blog is Perfecting OO's Small Classes and Short Methods, which presents a short series of stringent guidelines to help an imperative-trained developer master OO.
If I were to add one item to the list, it would be: Don't use return codes to indicate the status of an action. Developers trained in languages such as C have the habit of using return codes to indicate the success or the nature of failure of the work done by a function. This approach is used because of the lack of a structured exception mechanism. But when exceptions are part of the language, the use of status codes isa poor choice. Among the key reasons are: many status codes are easily ignored; developers will expect problems to be reported via the exception mechanism; exceptions are much more descriptive. And finally, exceptions enable return codes to be used for something useful--namely returning a data item.
Astute readers will note that in Java, null is frequently used as a return value to indicate a problem (as in Collections). This practice subverts the previous points, and it too should be avoided. Returning a null presents code with many problems it should not have to face. The first is the risk of a null-pointer blow-up because the return value was accessed without being checked. This leads to the code bloat of endless null value checks. A much better solution, which avoids this problem, is to return an empty item (empty string, empty collection, etc.). This too communicates that no data item fulfilled the function's mandate, but it does not risk the null-pointer problem, and it frequently requires no special code to handle the error condition.
Hence, if your OO code is characterized by heavy reliance on return codes (many of which I am certain are not checked), consider rewriting it in favor of exceptions and use return statements solely for returning non-null data items.
Sunday, September 28, 2008
Banishing Return Status Codes
Subscribe to:
Post Comments (Atom)
16 comments:
If the user searches the database, and no result is found, nothing bad happened, so an exception is not the best OO solution.
Create a static object, named NO_RESULTS_FOUND, of the base type object and return that.
No exception handling and no null return value.
(Effectively it is a null, but this may make the most orthodox OO fanatics happy)
Or, even better, return this, thus allowing call chaining...
Exceptions should be reserved for exceptional circumstances and not be used in normal flow of operation.
Look it up.
Herein lies madness. When I first moved to Python (around 2001) I shared a similar belief to the somewhat flawed one you hold now. After joining a corporation that will remain nameless I had this shaken firmly out of me, for (at a minimum) the following reasons:
- The use of exceptions introduces a whole new control flow that many developers, experienced and newbie alike, rarely think about until their production code explodes. Theoretically Java deals with this through checked exceptions, but therein dragons lie.
- Performancewise, every practical exceptions implementation favours the "unexceptional" case and greatly punishes the exceptional case. Stack unwinding code is not something you want to run in an inner loop of your millions-of-transactions-per-second application.
There are many examples of methods that may fail and produce no useful result, even when 50% or more of their typical input data will cause them to fail. As a poor example, consider a function that accepts freeform text which may be treated as some integer ID or a substring to be matched. The work involved in testing for a valid integer is only slightly less than the work involved in parsing that integer. In my hypothetical millions-of-tps system, these kind of optimisations often become important.
- There is nothing particularly wrong with signalling completion status using the return value of a procedure; indeed there is greater asymmetry in signalling failure using a completely different mechanism.
An addend to this is that there is nothing particularly right about using the type system to classify error codes; "is-subclass-of" and "is-integer-equal" are no better than each other, except in the case where a procedure is returning very rich error information. In that case the procedure likely has an interface problem.
In principle I like the idea, but after observing the success of a modern Java/C++ code base whose size I am unlikely to see paralleled again in my lifetime (and which you are very likely to use every day of your life), I have to disagree.
Love the blog! :)
dave w. is probably talking about google's codebase, and he's right. exceptions are disallowed and most errors are raised through return codes.
@droberts: Good advice!
@davidw: Yes, right. My post is general advice. The example contexts you discuss are special circumstances where, I agree, return codes would serve better. There is, as you state, a cost in both performance and complexity for exceptions that can be excessive in tight loops or complex contexts.
This is one of those posts on coding practices that comes along every once in a while that attempts to convince everyone that we should all abandon our ways in favor of a "better" methodology. (I'm guilty of this type of post, too, so I understand where the author is coming from)
Unfortunately, nothing like that will ever occur because sometimes returning an error code is handy.
That's not meant to negate the effectiveness of a good try catch, of course, but you have to look at each situation in its own way and code based off of its requirements during that time and for the future.
This argument is similar to the JavaScript argument that since innerHTML isn't part of or using the DOM specifications, we should all spend our days coding DOM manipulations instead of a quick-and-dirty innerHTML.
Sometimes quick-and-dirty isn't all that dirty.
I like haskells way of returning the status of an operation. In Haskell a function that may fail will return a value of type class Maybe.
http://en.wikibooks.org/wiki/Haskell/Hierarchical_libraries/Maybe
I concur with jessta, the Maybe type is pretty slick. The basic idea is that you can roll your status code and actual return value together into one, and then unpack it on the other side. It forces you to deal with the return code, because you have to consciously do the unpacking. The Maybe type attempts to cut down on code bloat by providing the monad bind (>>=) operation which basically does the thing you normally do with unhandled exceptions, which is to let them bubble up the stack. I'm hacking C now, and the lack of a way to bubble failure up the stack is driving me nuts with all the boilerplate.
Exceptions are for exceptions: events that you do not expect to occur on a regular basis. You should not use exceptions in place of return codes for normal processing due to the extra overhead and performance penalty of exceptions.
You might say a better rule would be "banish return status codes and use exceptions if your language is poor enough not to offer a suitable alternative".
The Java case of NULL return codes indicating an error is a case in point. Java doesn't have a powerful enough type system to express types which can or cannot be nil. In a decent language you can force the programmer to handle the error return code, eg:
type t = Success of result | Error of detail
val f : arg -> t
match f with
| Error detail -> (* got to handle the error *)
| Success result -> (* next operation *)
(And you can simplify the above code further and make handling the error even more forceful, using monads, but I won't go there for now).
Note that this is distinctly different from C, where it's very easy to ignore a return code from a function, or Java/Python/etc. where nothing forces you to catch the exception.
Yes, this is so much clearer:
try
{
GetDataFromDatabase();
}
catch FileNotFound
{
...
}
catch NoSuchIndex
{
...
}
catch InvalidUser
{
...
}
catch BadPermissions
{
...
}
catch DoesntWorkWithOracle
{
...
}
catch NotConnectedToNetwork
{
...
}
catch NetworkFailure
{
...
}
catch NotUsingAMac
{
....
}
With any discussion like this, it would also be good to reference the Null object design pattern, which eliminates any Null references at runtime. No need to check for Null if you design a system to never encounter it.
"Exceptions are for exceptions: events that you do not expect to occur on a regular basis. You should not use exceptions in place of return codes for normal processing due to the extra overhead and performance penalty of exceptions."
What about language features that are designed specifically to use exceptions in the normal course of events? Such as when your iterator throws StopIteration to tell you that it is done iterating?
@anonymous. I trust you're kidding. The problem is exactly the same with a return code. You have to some similar logic (presumably a switch statement with lots of cases) to distinguish the types of error the status code can report.
(this is anonymous)
Thank you for exactly understanding my point: to wit: it's exactly the same. If you want to handle "situations" in your code, you have to handle them.
Except that with exceptions (at least in C++), if every single resource allocated on a scope doesn't have an explict RAII type handler, you are bound to leak those resources.
If you don't want to handle them, then having status code is clearer.
Post a Comment