Considering Exceptions Harmful Considered Harmful
Joel Spolsky is a man whom I have come to respect because of his writing. I still do, and it will take more than one posting by him that I disagree with to change that.
But boy, do I disagree with his disagreement with exceptions. So do others (like Jesse Ezel in this post, Dare Obasanjo in this post, and with more brevity, Andy Smith with this post). So let me put in my two cents.
Let's say I have a function f(x), defined as 1/x. If I want to evaluate this over some domain, I may say y= f(x). Here's our basic function.... similar, you may say to a function in code: (lets stick to ints for input) ouble f(int x){ return 1D/x; }Great. Now what happens when I pass in 0?
Without exceptions, I have two choices.
The problems with the first approach are many, so let's list a few:
- For each type of error I want to return, I "lose" a valid result -- what if that's the result I wanted?.
- What if the precision of a double changes? I have a hole in my results, not just at the end. Say hello to the return of "magic" numbers!
- Someone using my function may not read the docs (heaven forbid), and use my "guard" value as an actual return value in blissful ignorance. Oops, hope he's not graphing from -1 to 1.
Okay, so that's probably not the best way-- for this function, every possible return value is valid data, so let's use the second approach... Oh, no ... more problems!
- (joel mentions this one, but says it's better than the exception)I can't say g(f(x)) -- I must say
result = f(x, out y);
if( result == 0)
result = g(y);
...
(when objects are returned, you lose the notation of f(x).g(y) for something similar).
- What about String.Format, and other variable-argument functions? out function as the first parameter?
- Which brings us to conventions. Every other function in the world returns data as the return... and sometimes error codes. Are all my functions going to use out params, or only some of them. Please don't say "case-by-case"
- Shall we ask about propery gets? Array accessors? how do we put a return value on these and an error... are these special cases (I hate special cases!), or are they requirements for new language design?
Then we have (sorry..) more: What about errors written by me, the author of f(x). I don't check for overflow, so you don't know about it. And, what about memory problems, threads aborting and other "errors" that don't occur in a function, or that a function knows about.
These are both great cases for exceptions.. If I have a finally block, my cleanup code will be called, barring the machine shutting off or other acts of god (like stack problems), even if I didn't know about the possibility of an error occuring. And better yet, I can't easily ignore the problem and hope it goes away. I have to do something explicit like this:
try{
y = f(x);
} catch(Exception){}
Compare that to
f(x,y);.. which is easier to ignore the error?
Finally (yes, I'm almost done, for those of you still here), as to the "magic goto" claim - the biggest problem with Joel's post - It's not magic, it's simple defensive development. Here's the mantra: Assume any single operation you perform can throw an exception. There, that's the magic. You don't have to handle each one - and shouldn't try. That's the beauty of exceptions. Just cleanup after yourself and let the problem go up the chain.
With error codes (either approach)you'd have to have some way to either handle every possible error that could occur, or notify your caller of every possible error that you can't handle.
Mr. Spolsky, I can't enumerate every single error that can occur, so I'll stick to exceptions. They may not be the perfect solution, but I think they're better than any alternative I've used.