Exceptions are .NET’s primary mechanism for communicating error conditions. Exceptions have great power, but with great power comes great responsibility. Like anything, exceptions can be abused, but that is no excuse to underuse them.
Compared to returning error codes, exceptions offer numerous advantages, such as being able to jump up many frames in a call stack and including as much information as you want.
Solution: Use the throw
syntax while creating the exception.
Solution: Wrap the code that could potentially throw an exception inside a try { }
followed by a catch { }
.
Solution: You can have multiple catch
blocks after a try
. .NET will go to the first catch
block that is polymorphically compatible with the thrown exception. If you have code that throws both ArgumentException
and ArgumentNullException
, for example, the order of your catch
blocks is important because ArgumentNullException
is a child class of ArgumentException
.
Here’s an example of what not to do:
Because ArgumentNullException
is a type of ArgumentException
, and ArgumentException
is first in the catch
list, it will be called.
The rule is: Always order exception catch
blocks in most-specific-first order.
Solution: There are two ways to do this, and the difference is important.
The naive (that is, usually wrong) way is this:
What’s so wrong with this? Whenever an exception is thrown, the current stack location is saved to the exception (see later in this chapter). When you rethrow like this, you replace the stack location that was originally in the exception with the one from this catch
block—probably not what you wanted, and a stumbling block during debugging. If you want to rethrow while preserving the original stack trace call, throw without the exception variable.
Here’s the difference in a sample program:
You can see that the first stack trace has lost the original source of the problem (DoSomething
), while the second has preserved it.
Be smart about when to intercept an exception. For example, you don’t usually want to log an exception and then rethrow at multiple levels, causing the same error to be logged over and over again. Often, logging should be handled at the highest level.
Also, beware of the trap of habitually handling exceptions at too low of a level. If you can’t do something intelligent about it, just let a higher level worry about it.
finally
Solution: Use finally
, which is guaranteed to run at the end of a try
or a try-catch
block. This happens whether you return, an exception is thrown, or execution continues normally to the line after the try-finally
.
The output for this program is as follows:
Note that a catch
block is not necessary when finally
is used.
Yes, finally
is guaranteed to run—except when it’s not. If your code forces an immediate process exit, the finally
block will not run.
Solution: Exceptions are very rich objects—far more powerful than simple return codes. Table 4.1 lists the properties available to all exception types.
Here’s an example:
This program produces the following output:
This is when the program is run in Debug mode. Note the differences when the program is run in Release mode:
There is less information in the stack trace when a program is compiled in Release mode because the binary does not contain all the information necessary to generate it.
Solution: Your exception can contain whatever data you desire, but there are a few guidelines to follow. In particular, you should have certain standard constructors:
• No arguments (default constructor).
• Takes a message (what is returned by the Message
property).
• Takes a message and an inner exception.
• Takes data specific to your exception.
• Takes serialization objects. Exceptions should always be serializable and should also override GetObjectData
(from ISerializable
).
Here is a sample exception that implements all of these recommendations:
Taking the time to implement exceptions as deeply as this ensures that they are flexible enough to be used in whatever circumstances they need to be.
Solution: A thrown exception is passed up the call stack until it finds a catch
block that can handle it. If no handler is found, the process will be shut down.
Fortunately, there is a way to trap unhandled exceptions and execute your own code before the application terminates (or even prevent it from terminating). The way to do it depends on the type of application. The following are excerpts demonstrating the appropriate steps. See the sample code for this chapter for full working examples.
When running these sample projects under Visual Studio, the debugger will catch the unhandled exceptions before your code does. Usually, you can just tell it to continue and let your code attempt to handle it.
In console programs, you can listen for the UnhandledException for the current AppDomain:
In Windows Forms, before any other code runs, you must tell the Application
object that you wish to handle the uncaught exceptions. Then, you can listen for a ThreadException
on the main thread.
In WPF, you listen for unhandled exceptions on the dispatcher:
In ASP.NET, you can trap unhandled exceptions at either the page level or the application level. To trap errors at the page level, look at the page’s Error
event. To trap errors at the application level, you must have a global application class (often in Global.asax
) and put your behavior in Application_Error
, like this:
Here are some general guidelines on exception handling:
• The .NET Framework uses exceptions extensively for error-handling and notification. So should you.
• Nevertheless, exceptions are for exceptional situations, not for controlling program flow. For a simple example, if an object can be null, check for null before using it, rather than relying on an exception being thrown. The same goes for division by zero, and many other simple error conditions.
• Another reason to reserve their use for exceptional error conditions is that they have performance cost, in both speed and memory usage.
• Pack your exceptions with as much useful information as you need to diagnose problems (with caveats given in the following bullets).
• Don’t show raw exceptions to users. They are for logging and for developers to use to fix the problem.
• Be careful about the information you reveal. Be aware that malicious users may use information from exceptions to gain an understanding about how the program works and any potential weaknesses.
• Don’t catch the root of all exceptions: System.Exception
. This will swallow all errors, when you should be forced to see and fix them. It is okay to catch System.Exception
, say for the purpose of logging, as long as you rethrow it.
• Wrap low-level exceptions in your own exceptions to hide implementation-level details. For example, if you have a collection class that is implemented internally using a List<T>
, you may want to hide ArgumentOutOfRangeException
s inside a MyComponentException
.