Exception Cost: When to throw and when not to

I wish I could speak intelligently on the exact cost but it's really quite difficult to project for any given usage, it's best measured for your specific cases. However there are a couple of different kinds of cost and they're both worth at least a few words.

The first kind of cost is the static cost of having exception handling in your code at all. Managed exceptions actually do comparatively well here, by which I mean the static cost can be much lower than say in C++. Why is this? Well, static cost is really incurred in two kinds of places: First, the actual sites of try/finally/catch/throw where there's code for those constructs. Second, in unmanged code, there's the stealth cost associated with keeping track of all the objects that must be destructed in the event that an exception is thrown. There's a considerable amount of cleanup logic that must be present and the sneaky part is that even code that doesn't itself throw or catch or otherwise have any overt use of exceptions still bears the burden of knowing how to clean up after itself.

In the managed world, there isn't this widespread notion of deterministic destruction of objects. This means that the static cost of exceptions can be quite a bit lower, because just creating new objects doesn't force you to update your exception state. Objects that become dead as a result of a thrown exception are handled no differently than other dead objects, they don't need special tables. So the static cost is really driven by overt use of exception features, and not so much implicit cleanup.

The second kind of cost is associated with actually throwing an exception. This is where the cost is higher in the managed world -- indeed it really couldn't possibly be lower because the same unmanaged exception throwing mechanism is used to actually get the exception started so we began at the unmanaged baseline. Where the extra cost comes in is the richness of features that are available in managed exceptions -- things like having the full call stack available in the exception object for display/interrogation. Those features cost space/time, and while we believe they offer an excellent deal in terms of increasing the ability to diagnose and correct exceptions you still need to be aware that the throw is not particularly cheap.

I have two “almost rules” for throwing exceptions, and a little bit of advice about exceptions in general. As usual my “almost rules” have exceptions and I leave it to the reader to identify them 🙂

Almost Rule #1

When deciding if you should throw an exception, pretend that the throw statement makes the computer beep 3 times, and sleep for 2 seconds. If you still want to throw under those circumstances, go for it.

Seriously, people throw much too often, and for things that aren't really very exceptional at all. In fact, my own opinion is that the framework itself throws far more often than it should. The actual throwing of exceptions is sufficiently costly that I try to limit it to cases where I'm certain that less than 1 call in 1000 (99.9%) to a particular service will require an exception. This means throwing exceptions on things like invalid arguments to an API is probably just fine, but on the other hand throwing an exception due to invalid user input, or badly formatted text from an external system, could be a bad idea. Significant use of exceptions in business logic validation is more often a bad idea than a good one, so be careful out there.

Almost Rule #2

If you think it will be at all normal for anyone to want to catch your exception, then probably it shouldn't be an exception at all.

Again, my favorite consumer of exceptions is a generic exception handler at a fairly high level on the stack. Anything that fundamentally needs to be dealt with at a lower level should be looked at suspiciously. Often such a beast would fail the “only 1/1000” of the time test. Since doing control flow with exceptions is alot more expensive/confusing than other forms of control flow its best avoided if the special handling is commonplace.

What I like to see is general purpose logic for state cleanup, transaction abort, reset and retry. What I don't like to see is “downgrading” an exception into a return code, or routinely being able to “ignore” the exception and proceed. Those latter cases are usually a bad sign.

Following these Almost Rules whenever possible will help keep you on the straight and narrow.

Hidden Exception Usage

Last but not least, don't forget that certain standard language features include try/finally implicitly. Both the “using” and “foreach” statement can require a try/finally in order to give the necessary semantics. Here's an interesting blog from Sudhakar Sadasivuni which discusses this for the using case. Foreach does a similar thing, as Mark Michaelis discusses.

In managed C++ I used to use stack based objects to avoid try/catch blocks when possible. The code was cleaner and there was no explicit set up of the try catch which was faster.

I now use a "using" when possible instead, and you mention that there is an associated try catch with it. I guess I knew that there was a try/catch underneath, but somehow I didn’t want to admit it to myself. A bit disappointing that I only have cleaner and more reusable code with a "using". Ah, well. It is nice to have the feature.

Yes, deterministic destruction would increase the cost to the standard level. Naturally we’re always trying to find clever ways to keep that to a minimum but even low(er) cost is more than none at all.

So, do you think you could speak to the folks who designed System.Messaging? That namespace abuses exceptions in the most horrific ways. For example, if you call a synchronous Receive or Peek method on a MessageQueue and you specify a timeout value, the object raises an exception when your timeout expires. That would be bad enough (raising an exception for an outcome that I actually asked for), but it raises the SAME exception for everything, forcing me to interrogate the MessageQueueErrorCode to distinguish between the expected result (the timeout) and all the real exceptional situations that might have occurred. Luckily for me, I use VB.NET, so I can use the "Catch … When" syntax. I pity the poor C# coders who have to deal with this crap.

The hidden exception usage case is a little misleading. When using try-finally constructs and flow control leaves the try block normally (i.e. no exception occurred) then you do not incur the overhead of the SEH stack walk that you would get had an exception been thrown and the finally block was executed on the 2nd pass as the stack was being unwound via a global unwind. There may be some overhead if bookkeeping is done to tables tracking the state of objects within the body of the method.

I’d be interested in how much overhead there is in a try-finally where an exception does not occur. It should be very low.

You are correct, in the normal case with the finally block and no exception all that has to happen is a fairly minor state change to indicate that the finally block need not be run. However it’s not nothing and such state changes do add up.

In unmananged code you could think of each destructor as being in its own implicit finally block and you wouldn’t be too far from the truth.

The fact that these implicit finally blocks can be absent in the managed world helps reduce the static cost.

System.Drawing routinely throws exceptions, which are often impossible to predict. Put a few hundred of these in a tight display loop… it becomes horrible. One example, is an extreme zoomout matrix, when displaying text. Throws an exception, but to predict when? Nearly impossible.

The DTE api (the VS automation object model) usually throws an exception to indicate an object cannot be found in a collection. Simply looking up a control on a command bar can throw an exception. This has also caused perf hits for us.

Thanks for the focus on this widespread problem! I hope it will lead to some changes in the MS libraries.

Unfortunately, it looks like both the FindFirstFile()/FindNextFile method that I mentioned ">yesterday and Richard’s #003900">VB .NET suggestionDir() really just calls the Win32 API functions behind the scenes). In both cases, when the code is looking for all of the subdirectories…