The following guidelines are excerpted from the book Practical .NET2 and C#2.
The exception mechanism is generally well understood but quite often used improperly. The base principle is that an application which functions in a normal way should not throw exceptions. This forces us to define what an abnormal situation is. There are three types of abnormal situations:
- Those which happen because of a problem with the execution environment but can be solved by a modification to this environment (missing file, invalid password, non-well-formed XML document, network unavailability, restricted security permissions…). Here, we are talking of business exceptions.
- Those which happen because of an execution environment problem which cannot be solved. For example, memory hungry applications such as SQL Server 2005 may be limited to 2 or 3 GB of addressing space in a 32-bits Windows process. Here, we are talking of asynchronous exceptions from the fact that they are not related to the semantics of the code which raised it. To manage this type of a problem, you must use advanced CLR 2.0 features such as Constrained Execution Regions (CERs) and Critical Finalizers. This is essentially equivalent to treating such abnormal situations as normal! Be aware that only large servers which push the limits of its resources should encounter asynchronous exceptions and will need to use these advanced mechanisms.
- Those which happen because of a bug, and which can only be solved by a patch which fixes the bug properly.
When you catch an exception, you can envision three scenarios:
- Either you are faced with a real problem, that you can address by fixing the conditions which cause the problem. For this, you may need new information (invalid password: ask the user to reenter the password...).
- Or you are faced with a problem which you cannot resolve at this level. In this case, the only good approach is to re-throw the exception. It is possible that there may not be a proper exception handler, and in this case, you delegate the decision to the CLR runtime host. In console or windowed applications, the runtime host causes the whole process to terminate. Note that you can use the
AppDomain.UnhandledException event which is triggered in this situation, in order to take over the termination of the process. You can take advantage of this ‘last chance’ to save your data (as with Word) without which this would definitely lead to data loss. In an ASP.NET context, an error processing mechanism is put in place.
- In theory, a third scenario can be envisioned. It is possible that the exception that was caught represents a false alarm. In practice, this never happens.
You must not catch an exception to simply log it and then re-throw it. To log exceptions and the code that they have traversed, we recommend using less intrusive approaches such as the use of specialized events of the
AppDomain class, or the analysis of the methods on the stack at the moment where the exception was thrown.
You must not release the resources that you have allocated when you catch an exception. Also, be aware that, in general, only unmanaged resources are susceptible of causing problems (such as memory leaks). This type of code to release resources must be placed in a
finally block or in a
Dispose() method. In C#, the
finally blocks are often implicitly encapsulated in a
using block which acts on objects implementing the
For a specific type of exception, asking this question comes down to asking yourself at which method depth this exception must be caught and what must be done about it. By method depth, we mean the number of calls embedded since the entry point (generally the
Main() method). This means that the method representing the entry point is the least deep. The answer to these two questions depends on the semantics of an exception. Ask yourself for each type of exception, at which depth your code is more apt to be able to correct the conditions which have triggered the exception and resume the execution, or to be able to properly terminate the application.
Generally, the deeper a method is, the less it must catch custom exceptions. The reason is that custom exceptions often have a signification to the business of your application. Hence, if you develop a class library, you must let exceptions which are meant to the client application bubble outside of the library.
You may be tempted to use exceptions instead of returning error codes in your methods, in order to indicate a potential problem. You must be careful, as the use of exceptions suffers from two major disadvantages:
- The code is hard to read. In fact, to understand the code, you must manually do the work of the CLR, which consists in traversing the calls until you find an exception handler. Even if you properly separate your calls into layers, the code is still difficult to read.
- Exception handling by the CLR is much more expensive in terms of performance than simply looking at an error code.
The fundamental rule mentioned at the beginning of this section can help you make this decision: an application which functions within normal conditions does not raise exceptions.
An abusive use of exceptions happen when we assume that, since we catch all exceptions, those provoked by eventual bugs will also be caught. We then assume that they will prevent the application from crashing. This reasoning does not take into account the fact that the main nuisances from bugs are from those which go uncaught, such as indeterminist, unexpected, or false results.