Fog Creek Software
Discussion Board




Exception handling

I was surprised to read your article favouring error-codes over exceptions. I have worked on both flavours of error-handling and personally find exception-handling elegant and _safer_ than error-codes. Let me explain :

They are not invisible in the source code, atleast not in java. Every method that has a 'throw' construct has to declare 'throws FooException' and every method that calls a method with such a declaration has to catch it or explicitly declare a 'throws'. This arrives at a neat hierarchy of methods specifying explicitly the exceptions handled and generated.

They do create many exit points, but join them with a 'finally' construct in addition to the 'try' and 'catch' whose block will be executed regardless of whether an exception is caught or not. Lets you wipe off those bloodstains..

Raymond Chen's article is one-siding the issue. If he can afford to define separate error-codes for each of the error-conditions, he might as well define different exceptions and add catch blocks.

What do you think?

Subhash
Friday, April 23, 2004

It is a complicated issue and there are many arguments on both sides.

My personal style is to use very, very simple code, and I avoid some more advanced language features because it makes code easier to debug.

To give an example of this from my life: CityDesk (a VB app) can be compiled in debug mode or non-debug mode, controlled by a global define called afDebug which is either 0 or 1. To write code that only runs in non-debug mode you're supposed to write:

#If afdebug = 0 Then
      ' code here
#End If

At some point someone had inadvertently written

#If Not afdebug Then
        ' code here
#End If

Of course I assumed this was fine. Of course in Visual Basic Not(1) is -2, which is not False, which surprised me after a decade of experience with Visual Basic. Although intellectually I remembered that VB uses -1 for True, not 1, the code's behavior surprised me.

The phrase "the code's behavior surprised me" should always be a big, gigantic red flag... that's where a ton of bugs come from. And so I always try to use the minimal set of basic language features that can get the job done, and I'm terrified of features that can cause a given line of code to behave in different ways depending on something which you can't see. Thus, templates, operator overloading, type cast overloading, and exceptions are all like 20,000 volt wires to me -- useful, yes, possible to use correctly, yes, but very, very dangerous.

Joel Spolsky
Fog Creek Software
Friday, April 23, 2004

Subhash, you forgot Runtime (unchecked) exceptions, which can be caught but which don't need to be specified in the throws clause.

There's a school of thought in Java that I subscribe to that says that you should consider whether client code catching an exception can reasonably do anything with it when it's caught. If not, then make it a Runtime exception.

For example, (in a typical J2EE application) if the database has gone belly up then there's not a lot I can do at that point other than to show some sort of intelligible message to the user rather than a scary stack trace. Forcing me to litter my code with catch blocks to handle exceptions that I can't do anything meaningful with is counter productive.

John Topley (www.johntopley.com)
Friday, April 23, 2004

Raymond C's article is correct, but the problem he points out is not unique to exception handling, and easily fixed by adopting the Resource-Acquisition-Is-Initialization pattern (in a commit/rollback flavor).

I can't see how any large development effort gets by without exception handling. Don't you get tired of writing all those error checks and manual clean-up code?

Dan Maas
Friday, April 23, 2004

I'm with Joel in not using Exceptions. My objection, however, it based on my experiences with using the things. In my experience (C++, not Java. Things may be better elsewhere) if an exception gets away from the immediate vicinity of being thrown you end up with almost no information as to where it came from. The system I work on has about 400-500K lines of code (C/C++ mix, with lots of CORBA interfaces running around). Now when a poorly thought out exception burried somewhere deep in the heart of the mess occurs, what happens? The exception handler all the way up at main() ends up with it! How does this help? Even assuming I have an exception handler of appropriate type at the top (I do these days), I get NO information about what went wrong (OK, that's not wholely true. I often get where the exception was thrown from. Which is invariably in one of our library classes, which are used in hundreds of places around the system).

I'm not saying exceptions are always bad, but in my experience it's a lot easier to debug ones way back to where an error code was missed, rather than back to where an exception was thrown.

I guess my problem with exceptions is the blasted things get in the way of any attempt to debug my code.  To be honest, my present system would be MUCH better off without exceptions. (Of course, it would also be much better off without dozens of poorly thought out and even more poorly written wrappers around OS functionality. But that's the fault of the exceptions).
\

Michael Kohne
Friday, April 23, 2004

There's a marked difference in the value of exceptions in C++ and the value of exceptions in a meta-data driven language like Java or C#.

Of course, much of the "cool" stuff in C++ is done much better in the newer languages. After more than a decade as a professional C++ program, and now 2 years on the .NET platform, I don't think I could ever go back.

Brad Wilson (dotnetguy.techieswithcats.com)
Friday, April 23, 2004

"Now when a poorly thought out exception burried somewhere deep in the heart of the mess occurs, what happens? The exception handler all the way up at main() ends up with it! How does this help?"

You miss the point about exceptions..  it's not about handling them, it's about NOT handling them.  If you have an exception handler in main() probably the best you can do is show an error and die.  But in C++, assuming RAII, all your destructors have been called and your so application has cleaning up all the resources it's opened (roll back transactions, close files/databases, etc) before it dies.

You don't raise exceptions for errors that you can handle locally.  You raise exceptions for errors that you cannot handle locally!

Almost Anonymous
Friday, April 23, 2004

I should point out that I use only one class for exceptions throughout all my software. It encapsulates a string describing exactly what went wrong (e.g. throw MyException("error reading from file foo")). I've found this a lot easier to manage than a complex class hierarchy of exceptions.

(as an exercise for the reader, the string can be internationalized and used as the user-visible error message if anything goes wrong)

Dan Maas
Friday, April 23, 2004

(sorry to append)

It seems like there are two universes of error handling: manual cleanup and RAII (with exception-safe, commit/rollback classes). The problem is that they are mostly incompatible. It is tedious to connect code that uses one model to code that uses the other, and very difficult to "port" an existing codebase from one model to the other (since it cannot be done incrementally, at least as far as I can see). You are pretty much stuck with whatever model you start out in.

Dan Maas
Friday, April 23, 2004

In regards to the comment that exceptions make it harder to debug:

At least in Visual C++, you can set the debugger to break when an exception is thrown, rather than waiting until the catch, so it's pretty easy (once you set it up properly) to see where the exception is coming from.

Or, in .NET, you get a stack trace with the exception, which makes things a whole lot easier.

In "modern C++", you're pretty much required to go the exception/RAII route. The reason is that the runtime library WILL throw exceptions at you. If you don't wrap your "return code" style code in RAII wrappers, then you'll leak when you get one of the runtime exceptions.

Chris Tavares
Friday, April 23, 2004

Yeah, using other code that goes that direction is what brought me down that gilded pathway.  It's much easier to wrap error codes and translate to exceptions than vice versa, exceptions having a lot more bits available.  And it certainly depends on where you use it; in efficiency, more bits is bad.  And in some languages, the native exception handling facilities are crappy and hard to use, and no one will do much with them until you get them to the "don't make me think" level.  It takes time to build up local standards and abstractions that make sense.  (Just finished a fourth refactor of ours -- happy with it so far...)  Don't get me wrong -- I like *using* exceptions now that I'm used to them.  I am much happier reading code with responsibly written exceptions than responsibly written error codes.  And I'd rather receive a responsible exception -- I often won't have to look anything up to figure out what I broke.

It isn't the silver bullet people make it out to be, though, because what matters is still **responsibly written.**  It's really easy to write "throw makeUselessException();" whether lazily or incompetently.  Of course, the guys who write that way never used to return anything but -1 or 0, either.  So their exceptions are no better than their error codes, but my exceptions are more useful, and we interface with third parties better now.  I think we're a bit better off.

Mikayla
Friday, April 23, 2004

I think there is definately a fundamental split in styles between RAII/exception style and the error code style, which probably explains the animosity between the C and C++ camps.

I think the split boils down to more vs less.  More explicit code, but also more code vs less explicit code but also less code. Then there is, of course, just bad code.

My personal preference is that I would much rather work carefully craft 100 lines of precision code than 1000 lines of basic code.  The larger code may be more readable to a beginner, but once the idioms are understood the 100 lines are easier to read and maintain.

Tito
Friday, April 23, 2004

That also makes sense in light of the fact that bugs per line of code tends to remain constant.

Dan Maas
Friday, April 23, 2004

I can admit there might be room for debate in the C++ world, since C++ exception handling is rather limited.  In the modern languages used for business applications, like Java, it isn't even a discussion worth having.  Exceptions win, hands down.

Exceptions can carry all relevant context (including a complete trace down to the problem point) from where conclusive decisions should rarely be made (deep down) to where the best decision can be made for that situation, usually quite high up.

What should a low level data-writing operation do?  Report failure to the user?  How so?  Was that routine called from a GUI this time or a server application?  Who knows.  You can't even be sure the failed step is relevant to the overall activity.  What if the caller just wanted to *try* the operation?  Such decisions are contrary a basic tenet of modular programming: I don't assume anything about my caller but what they've told me.

Exceptions are awesome.  They help me make my code super-clean, reliable -- failing with predictable horror-case behavior, easy to maintain and understand, and as easy to reuse as can be.

catcher
Friday, April 23, 2004

Joel,

I agree with your concerns about exception-handling in C++. And I think the root-cause of that is exception-handling being slapped as an afterthought and not integrated as a first class language construct. I imagined C# would solve that, doesn't it?

John,

I classify runtime exceptions as errors, not Exceptions. Examples of errors are OutOfMemory, DivideByZero, NetworkDown etc. while exceptions would be of the class of DuplicateUser, RecordLocked etc. Errors are bad, bad things that should never happen, that you never _anticipate_. Whereas Exceptions are "exceptional" scenarios from which your code can possibly recover. There is no recovery possible for errors, except for a graceful exit and report to the user, and so even in the error-code-returning paradigm, one does not define a error-code for these, only a possible "42" catch-all error code reporting "Internal error" to the user.

My usual style is this : For the functions I invoke, I handle all the exceptions I can and throw the ones I can't. For the exceptions that can occur in my code, I explicitly throw a well-defined Exception (never an Error because that can be ignored). This forces the client-code to either handle or rethrow the Exception. This leads to a rather neat hierarchy of throws upto the main script for Exceptions that cannot be handled.

Subhash
Saturday, April 24, 2004

Don't laugh!  MFC actually is the only place that I've seen exception handling work well.

In MFC, to open a file, there is this big, gnarly trail of virtual functions in multiple objects that all get called.  Anywhere along the way, you can throw an exception and MFC will catch it at the top and show a "Could not open document" error message and the application continues on its merry way.

It works because "trying to open a file" in a shrinkwrap app has two special properties: (1) that there is actually something reasonable to do when the exception occurs that really (not just theoretically) allows the app to continue being useful and (2) that function is non-trivial.  To generalize, careful framework designers can use exceptions to "mark" off sections of user code as exception-safe and provide a real "backstop" to make it work.  Rather than treating exceptions as a general error handling mechanism, frameworks can say, "Hey, I've provided a backstop for these areas here and here, so go ahead and use exceptions but do your own error handling everywhere else."

A second point, also learned from MFC, is that 99% of the time (in shrinkwrap apps, at least), ASSERTs are preferred over exceptions.  If you are liberal enough with ASSERTs, the ASSERTs themselves will tell you what can realistically happen (while you unit test) and what is really just meant to be an ASSERT.  When you trigger an ASSERT in unit testing, you can simply replace the ASSERT with real error handling code, now that you have a concrete test that demonstrates what can happen.

Finally, if you don't trigger exceptions during development to see if your exception handling code works, the exception isn't likely to work during production.  Putting the exception there and hoping that somebody catches it isn't good enough.

Daniel Howard
Saturday, April 24, 2004

I think the best tactic is, as always, to use the right tool for each particular job.

Use exceptions where you need to get out of a deep/messy situation where a lot of resource cleanup needs to be done. Using error codes for such things usually leads to a real mess - maybe doubling the size/complexity of the code.

eg. A file reader, where a simple "catch" higher up can tell the user "failed to open file" and that's the end of the story.

Error codes are good for small, atomic functions, eg. fwrite() or malloc(). My life, and probably yours, would be really horrible if either of those two threw an exception.

Joe Cuervo
Sunday, April 25, 2004

I have seen asserts abused far too often to be comfortable with them.  I'm not quite ready to write them off entirely, but they make me nervous in anything beyond unit test cases.

I've seen to many people write:
ASSERT( SUCCEEDED(hresult) ) ;

It goes back to the issue of your release code not matching your debug version.  If you are concerned enough to check something in the debug build (by asserting), why aren't you concerned about it in the release build?


To comment on the item Joel posted on the main page (paraphrsed: "with error code you can easily tell when an error has been handled")  I disagree.  It only seems like you can.  It is very hard to see if a given function is supposed to return an error code.  Sure for API calls you have a chance of memorizing them all, but if you are using Error Codes for error handling throughout the application, you can't know what every function is supposed to return with.  Even worse, there can be a switch/case statement that doesn't handle all possible returns.

The biggest issue with return codes, is that your error handling comes to dominate your code.  This is because when you write a function, you have to write code that propogates every kind of error, even ones you don't know how to deal with.

With exceptions, you handle the ones the current function "knows how to" (meaning the ones that the current function has the information and scope to handle) and you can let the others pass upwards without having to write extra code in every function to do that.  All that extra "propogate error code upwards" code will have bugs in it too.

That being said, I do agree that C++'s exceptions could be improved.  I would like to see all exceptions derive from std::exception, akin to Java and C#, to make handling easier.  "catch(...)" just doesn't work very well.

Tito
Monday, April 26, 2004

Joel mentioned that it was unfortunate that you can't nest function calls in most popular languages if you want to simulate "multiple return types", i.e. either the real result or an error value.

One could return a struct (off the top of my head, not programmed in a while):

typedef struct {
  bool ok: // true iff no error produced, good is well defined
  union { NormalValue good; ErrorCond bad; } result;
} Multiple;

Then, if you could prove a function g() could not fail in a particular context, the nest

multresult=  f(g());

would be ok. The function f() can check the value it's passed if appropriate too.

Just an idea (and not original) - keep up the good work.

Ian Cottam
Monday, April 26, 2004

As people have mentioned, exceptions provide a GREAT tool for post-release bug finding - a stack trace provides every function that was on the call stack when the exception was thrown, so you can actually figure out where your program threw the exception. (It works great for pre-release testing to) Someone mentioned that their program threw an exception and they didn't know where it happened.

Secondly, I love how you can say "No known errors will screw up code above this point" by using a try / catch block that catches everything. With return codes, you have to be very careful to check every line of code for return codes, and hope that they are all documented properly.

Will Gayther
Friday, July 16, 2004

*  Recent Topics

*  Fog Creek Home