The Cost of Exceptions
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.