Errors and unexpected behavior in .NET applications are handled through a mechanism called structured exception handling, or SEH for short. This mechanism automatically collects and preserves information about errors and unexpected behavior in the form of an exception. An exception class is roughly equivalent to the Err class that you used in VB.Classic, although exception classes are much more flexible and powerful than the Err class. The following are some of the advantages that exceptions provide. I expand on several of these points in the next section.
You can add custom information to your exceptions by inheriting from the ApplicationException class and adding new properties. These custom exceptions retain all of the standard exception functionality while allowing you to add new exception information that helps other developers to understand the exceptions better. You can even create a complete exception hierarchy for your assembly or component. This concept replaces , and is much superior to, the VB.Classic idea of defining and raising new error numbers to represent errors that are unique to your component.
Custom exceptions are unique to the namespace in which they're defined, which means that you'll never have to worry about clashing with another component's custom exceptions. This was a big problem in VB.Classic, because the custom error numbers that you defined could easily be duplicated by other components. Any developer using both of your components would have no easy way of distinguishing between errors raised by the two components .
The Try Catch Finally mechanism provided by VB .NET to implement SEH allows you to isolate your exception management code in a single location, which means you can write recovery and cleanup code separately from your business logic, and you can guarantee that it will execute when necessary.
You can catch and deal with exceptions using sophisticated exception filtering, which allows you to deal with different exceptions at the precise level of granularity that you feel is necessary for each exception.
Exception hierarchies allow you to catch a set of related exceptions using just a single statement.
If an exception causes another exception to be thrown while it's being handled, the original exception is automatically appended to the new exception using the InnerException property. In this way, exceptions can be stacked without any loss of information. You can also use the GetBaseException method to traverse a list of inner exceptions and find the exception that was originally thrown.
Every exception has a built-in StackTrace property, which shows the complete stack trace from the point that the exception was thrown. Gone are the old days when you had to add the call stack information as a part of the error handling in literally every VB.Classic method. Now you can easily record the call stack information at the point where you catch the exception.
The System.Exception class is the base class for all CLS-compliant exceptions. Table 13-1 describes the properties of this class and also shows the equivalent VB.Classic Err properties.
Text explaining why the exception was thrown.
Text containing the name of the assembly that threw the exception.
A MethodBase containing the method that threw the exception.
Text containing a list of names and signatures of the methods in the call stack leading to where the exception was thrown. Note that code optimization by the compiler may result in some methods being omitted from this string.
Text containing a URL to information about the exception that might be useful to a developer or user .
HelpContext and HelpFile
If the current exception was caused by another exception, this is where the previous exception is stored. Otherwise this property is set to Nothing .
A protected property containing an integer used to store an error number raised by unmanaged code (such as VB.Classic). Any error raised from unmanaged code appears to managed code as an exception, with the error number stored here. To read this value, you should either pass the exception to the System.Runtime.InteropServices.Marshal shared GetHRForException method or catch the ExternalException type and use its ErrorCode property instead.
System.Exception sits at the base of a large hierarchy, or tree, of exception types. The benefit of this hierarchy, apart from grouping related exceptions, is that whenever you write code to catch a specific exception, you also catch all of its derived exceptions. For instance, writing code to catch ArithmeticException will automatically catch DivideByZeroException , NotFiniteNumberException , and OverflowException . This can have both benefits and problems. One benefit is that it's often easier to write code to catch and deal with the parent exception without having to worry about every individual exception derived from the parent. Another benefit is that whenever a developer adds a new exception derived from an exception that you're already catching, your code will automatically catch this new exception. However, the major problem with this approach is that your code won't necessarily know what to do with this new exception, and as you'll see shortly, you should rarely catch exceptions that you don't know how to handle.
The two most important exceptions derived directly from System.Exception are System.SystemException and System.ApplicationException . SystemException is designed for exceptions thrown by the CLR and .NET Framework class library, so that your code can be notified about any runtime or Framework problems. On the other hand, ApplicationException is designed for use by .NET applications. Though this separation is useful, it isn't always valid. For example, the .NET Framework class library derives IsolatedStorageException directly from Exception rather than SystemException , and the .NET Framework also derives some reflection-related exceptions from ApplicationException . In addition, your own applications might want to use exceptions derived from SystemException rather than ApplicationException in certain situations, such as using ArgumentNullException to signal that a null argument has been passed to a method or using ArgumentOutOfRangeException to signal an argument containing a bad value.
Before I go into more detail on managing exceptions, the next section is an explanation of the code that you have to write to handle exceptions.
Try Catch Finally is the mechanism that VB .NET provides for protecting your code from exceptions. It allows you to place any code that you wish to protect from exceptions within a Try End Try block, and then add a Catch block for each exception that you anticipate and want to handle. Each of these Catch blocks has an exception filter, which is an expression designed to catch a specific exception or set of exceptions. Within each Catch block you write code to handle that specific exception. Lastly, you use a Finally block to place any cleanup code, which consists of statements that must execute regardless of whether an exception was thrown.
A Try End Try block might want to handle several different exceptions or indeed do no exception handling, so the number of Catch blocks is optional, from zero upward. Exception cleanup is also optional, so you can have either one Finally block or no Finally blocks. The only requirement is that your Try End Try block must contain either one Catch block or one Finally block. This makes sense because there would be no point in protecting code that needs no exception recovery or cleanup when it fails.
Listing 13-1 shows some pseudo-code demonstrating where you should place code when writing a typical Try End Try block.
Try 'One or more statements that you wish to protect from exceptions goes here. Catch ExcType1 As Type1Exception 'This Catch block has an exception filter that catches a type 1 exception. 'Code that recovers from this exception goes here. 'This code will be executed automatically when a type 1 exception occurs, 'or when any exception derived from a type 1 exception occurs. Catch ExcType2 As Type2Exception When Expression1 is true 'This Catch block has an exception filter that catches a 'type 2 exception only when expression1 evaluates to true. 'Code that recovers from this exception goes here, and this code will 'be executed automatically when the exception filter conditions are met. Catch ExcType2 As Type2Exception When Expression2 is true 'This Catch block has an exception filter that catches a 'type 2 exception only when expression2 evaluates to true. 'Code that recovers from this exception goes here, and this code will 'be executed automatically when the exception filter conditions are met. Finally 'Cleanup code that must run whether or not an exception was thrown goes here. 'This code is guaranteed to execute, even if an exception occurs in the Try 'block or a Catch block. End Try
As you can see from this pseudo-code listing, the code that you write to deal with each exception is isolated in a single location and is guaranteed to execute whenever that exception (or an exception derived from it) occurs. This makes your error-handling code easier to maintain and separates it from the code dealing with the main business logic. This code partitioning has the added benefit of allowing you to partition your business thinking from your error thinking, which helps to reduce the severe mental load that inevitably comes as a part of software development.
Having seen how Try Catch Finally is designed to make the task of exception recovery and cleanup much easier, you need to see exactly how the CLR transfers control between blocks of code. When a statement in a Try block throws an exception, the following sequence of events happens:
The CLR walks sequentially down the list of Catch blocks within the local Try End Try block, looking for a local Catch block with an exception filter matching the exception that was thrown.
If a local Catch block has an exception filter that matches the exact exception that was thrown, the code in that Catch block is executed, followed by the code in the Finally block. Then execution continues at the first statement following the End Try .
Alternatively, if the exception that was thrown derives from the exception specified by a local Catch block, the same actions happen as described in step 2. For example, an exception filter that catches ArgumentException will also catch exceptions derived from ArgumentException , such as ArgumentNullException , InvalidEnumArgumentException , DuplicateWaitObjectException , and ArgumentOutOfRangeException .
If no local Catch block matches the exception that was thrown, the CLR walks back up the call stack, method by method, looking for a Catch block that wants to respond to the exception. If no matching Catch block is found in the call stack, the exception is considered to be unhandled. ( Please see the later section "Dealing with Unhandled Exceptions" for a discussion on dealing with these.)
Alternatively, if a matching Catch block is found somewhere in the call stack, the code in every Finally block between the throw and the catch is executed. This starts with the Finally belonging to the Try block where the exception was thrown and finishes with the Finally in the method below the method where the exception was caught.
After this cleanup has been completed for all methods below where the exception was caught, control is transferred to the Catch block that caught the exception, and this code is executed. Next to run is the Finally block of the Try where the exception was caught. Now that the call stack has been unwound and the error cleanup has been completed, the final step is to continue execution at the first statement following the End Try where the exception was caught.
If code within a Catch block causes another exception to be thrown, the original exception is automatically appended to the new exception using the InnerException property. In this way, exceptions can be stacked without any loss of information.
You should avoid placing cleanup code within a Finally block that might throw an exception, unless that code is within its own Try block. Without this added protection, the CLR behaves as though the new exception was thrown after the end after the Finally block and looks up the call stack for a remote Catch block that wants to respond to the new exception. The original exception will be lost unless the original Catch block saved it.
Notice that a statement that isn't placed within a local Try End Try block may still be protected from an exception. This is because a Catch block active in the call stack above the method where the exception is thrown might catch that exception. Only if the CLR can't find any matching Catch block in the call stack is the exception considered to be unhandled.
Some developers question the need for a Finally clause, pointing out that code added after the End Try statement will always be executed. This is normally true, but not always. Code placed in the Finally section is always executed, even after an Exit Sub , a Response.Redirect , or an exception thrown within a Catch clause, and these are examples of situations where code placed after an End Try statement won't be executed.
How you handle exceptions depends to a large extent on whether you're writing application code or library code. When you're writing library classes and methods that are going to be used by other developers rather than by end users directly, you should really propagate every exception unless you're deliberately providing a better level of abstraction or hiding meaningless implementation details. On the other hand, writing business logic and application code that interacts with end users means that you should catch nearly every exception and transform it into a context-sensitive message that makes more sense to the end user and the task that he or she is attempting to perform.
Whenever you write exception recovery code, you're faced with choosing one of several options for dealing with each exception. The following sections give you some general guidance about which option is best for individual situations. Please bear in mind that these are only general guidelines ”you should analyze each specific situation that arises in your own code.
If you ignore an exception, it will automatically bubble up to the calling code. Before disrupting this process by catching an exception, you should be clear about exactly what you're doing. There are really only five reasons to catch an exception:
Execute recovery code: You may explicitly need to recover from certain exceptions, for instance by reversing a transaction that failed before it completed. This strategy can involve swallowing the exception or rethrowing it, depending on circumstances.
Execute cleanup code: You may need to clean up after certain exceptions, for instance by closing a file or database connection.
Add more relevant exception information: You may need to add extra information to an exception before rethrowing it or throw a more meaningful exception.
Log exception information: In some circumstances, especially for exceptions that have reached the end user without being intercepted, you might want to log the exception information for later analysis.
Prevent an exception from reaching the end user: If an exception bubbles up through the call stack to reach the end user without being intercepted, you should usually intercept the exception at the user interface and translate it into a user-friendly message that is context-sensitive to the task the user is attempting to perform.
If you catch an exception, you're making an explicit statement that you expected that exception, you understand it, and you're going to deal with it. Unless you need to perform one or more of these five tasks , you should ignore the exception and let it propagate upward.
This strategy is the opposite of the previous one, in that System.Exception is the base of all CLS-compliant exceptions and catching it means catching all exceptions. If you catch and swallow System.Exception , you're saying that you understand and know how to deal with every type of exception. This is normally a bad practice, because there's no possible way for you to anticipate and understand every possible type of exception. You should therefore be very wary of any real-life code that uses Try Catch Finally like this:
Function CalcRatio(ByVal Top As Integer, ByVal Bottom As Integer) As Int32 Try Return Top \ Bottom Catch Exc As Exception Return 0 End Try End Function
Although you might think that division by zero is the only exception that this method can throw, and therefore it's safe to catch System.Exception , you would be wrong. Passing or Nothing for the second argument would indeed lead to a division by zero, but the CLR could also throw (for example) StackOverflowException . The catch in this method would then be hiding a potentially fatal exception, possibly leading to corruption of data or other bad side effects. The obvious answer in this case is to catch just DivideByZeroException .
Another way of looking at catching System.Exception is that it's the equivalent of the dodgy VB.Classic technique that involves using On Error Resume Next to protect VB.Classic code. This technique is sometimes called "nailing the corpse in an upright position," because it's saying that whatever happens, even if I've been shot through the heart, I should still keep running. Using this technique is like creating a "black hole" into which all errors coming from lower down in the call stack are sucked, never to emerge again.
You should especially not catch and swallow System.Exception in any method that's part of a class library, because this practice specifically denies any information to the developer that's calling your method. You often don't know what the calling code expects, and therefore you can't usually anticipate what a developer expects to happen when he or she calls your method.
During program development, some developers like to catch and swallow System.Exception in the last Catch block of each Try End Try block in order to display any unhandled exception on the console using Exception.ToString . The idea is that this helps to isolate errors during development, and that these Catch clauses will be removed before the application is moved into acceptance testing or production. There are two problems with this approach. The first problem is that you might forget to remove some or all of these dodgy Catch blocks. The second problem is that you'll find it impossible to test how exceptions are handled when they propagate up the call stack of your application. If you're tempted to try this approach, you should always rethrow the exception using an empty Throw statement, as discussed in the next section.
The only place where it might be acceptable to catch and swallow System.Exception is in your application's unhandled exception handler, otherwise known as a "last chance" exception handler. This is because you want to keep your application going for long enough to allow your end user to save his or her work. However, even here there are some exceptions that are dangerous to catch. See the "Dealing with Unhandled Exceptions" and "Handling Special Exceptions" sections later in this chapter for more details.
One situation where catching System.Exception as discussed in the previous section is not a heinous sin is when, after recovering from the exception, you always rethrow the same exception that you caught. There are some cases where you may need to recover from every possible exception so that you can restore your application to a safe state. For instance, in the following method you want to reverse the first money transfer if the second transfer doesn't happen, regardless of the reason why the second transfer doesn't occur. Notice that you can't reverse the first transfer in the Finally block because you only want to do the reversal when something goes wrong.
Sub AccountTransfer(ByVal AccountFrom As BankAccount, _ ByVal AccountTo As BankAccount, _ ByVal Amount As Decimal) 'Make first transfer (deduct amount from first account) AccountFrom.Balance -= Amount Try 'Make second transfer (add amount to second account) AccountTo.Balance += Amount Catch Exc As Exception 'Something went wrong with second transfer, so reverse first transfer AccountFrom.Balance += Amount 'Re-throw this exception, because we don't know how to handle it here Throw End Try End Sub
After performing the reversal, you should always rethrow whatever exception you caught, because you can't possibly know how to deal with the generic System.Exception .
Note that although using Throw means that the original exception type is preserved, the stack trace starts from the line that you rethrow the exception, not from the line where the exception was originally thrown. So you lose some information (the line number) if the Try block contains multiple statements.
You might wish to improve the quality of certain exception messages emitted from the code of other developers. As an example, I'm going to examine the possible exception messages that the System.IO.File.Move method can produce, and see where and how these messages need improvement.
Listing 13-2 shows some attempts to probe the File.Move method and groups the probes that produce unclear exception messages. I created a folder with the name of C:\Demo and placed in it two files, one named Source.txt and the other named Destination.txt. The latter file was set to be read-only. Then I executed the program shown in Listing 13-2 and made a note of any exception that produced unclear information. The program makes one test at a time, and each resulting exception is written to the console, together with a string describing the test.
Option Strict On Imports System.IO Module ExceptionWrapper Sub Main() Dim WrapperNo As Boolean = False, WrapperYes As Boolean = True 'MOVE commands producing good exception information TestMove("C:\Demo\Source.txt", "C:\Demo\Destination.txt", WrapperNo, _ "Destination is read-only") TestMove("C:\Demo\Source.txt", "", WrapperNo, _ "Destination is empty") TestMove("C:\Demo\Source.txt", Nothing, WrapperNo, _ "Destination is nothing") TestMove("C:\Demo\Source.txt", "C:\Dem\Destination.txt", WrapperNo, _ "Wrong destination folder") TestMove("C:\Demo\SourceX.txt", "C:\Demo\Destination.txt", WrapperNo, _ "Source doesn't exist") 'MOVE commands producing bad exception information TestMove("C:\Demo\Source.txt", "C:\Demo\*.txt", WrapperNo, _ "Wildcard in destination") TestMove("C:\Demo\Sou<rce.txt", "C:\Demo\Destination.txt", WrapperNo, _ "Bad character in source") TestMove("C:\Demo\Source.txt" & Space(300), "C:\Demo\Destination.txt", _ WrapperNo, "Source too long") TestMove("C:\Demo\Source.txt", "C:\Demo\", WrapperNo, _ "Destination is folder") TestMove("C:\Demo\Sou:rce.txt", "C:\Demo\Destination.txt", WrapperNo, _ "Dodgy character in source") End Sub Sub TestMove(ByVal SourceFile As String, ByVal DestinationFile As String, _ ByVal UseWrapper As Boolean, ByVal ProbeType As string) Try If UseWrapper = True Then MoveFile(SourceFile, DestinationFile) Else File.Move(SourceFile, DestinationFile) End If Catch ShowException As Exception Console.WriteLine("TESTING: " & ProbeType) Console.WriteLine(ShowException.ToString) Console.WriteLine() Console.ReadLine() End Try End Sub End Module
As you can see if you examine the exceptions produced by this program, some of the exception messages aren't clear, mainly because they omit the source and destination arguments to the File.Move method. If you were a developer tasked with understanding this exception log without having access to these arguments, it would be impossible to determine the exact problem given the information currently provided. For instance, how would you go about trying to decipher this exception?
System.ArgumentException: Illegal characters in path. at System.Security.Permissions.FileIOPermission.HasIllegalCharacters(String str) at System.Security.Permissions.FileIOPermission.AddPathList(FileIOPermissionA ccess access, String pathList, Boolean checkForDuplicates, Boolean needFullPath) at System.Security.Permissions.FileIOPermission..ctor(FileIOPermissionAccess access, String pathList, Boolean checkForDuplicates, Boolean needFullPath) at System.IO.File.Move(String sourceFileName, String destFileName) at ExceptionExperiments.Experiments.Main() in C:\Documents and Settings\Admin istrator\My Documents\Visual Studio Projects\ExceptionExperiments\Experiments.vb :line 13
One way to improve the quality of these exception messages is to write a wrapper method around the File.Move method. Within the wrapper, you decide to catch only the specific exceptions that you've determined are unclear, add your own improved exception message, and then throw an exception of the same type containing your improved exception message. You also keep the original exception as an inner exception to your new exception, in order to preserve the original exception information. This approach is a good way of improving thirdparty exception messages by making them more detailed and informative.
A possible wrapper for File.Move is shown in Listing 13-3. It catches all of the exceptions that the program shown in Listing 13-2 determined weren't very informative and adds some better diagnostic information, usually the values contained in each of the arguments. If you run the tests shown in Listing 13-2 with the UseWrapper argument set to true , the wrapper method shown in Listing 13-3 will be executed instead of using the File.Move method directly.
Sub MoveFile(ByVal SourceFile As String, ByVal DestinationFile As String) Try File.Move(SourceFile, DestinationFile) Catch Exc As ArgumentException _ When Exc.Message = "Illegal characters in path." Dim HelpfulMessage As String = Exc.Message & vbCrLf HelpfulMessage += "Source = " & SourceFile & vbCrLf HelpfulMessage += "Destination = " & DestinationFile & vbCrLf Throw New ArgumentException(HelpfulMessage, Exc) Catch Exc As ArgumentException _ When Exc.Message = "The path contains illegal characters." Dim HelpfulMessage As String = Exc.Message & vbCrLf HelpfulMessage += "Source = " & SourceFile & vbCrLf HelpfulMessage += "Destination = " & DestinationFile Throw New ArgumentException(HelpfulMessage, Exc) Catch Exc As IOException _ When Exc.Message = _ "Cannot create a file when that file already exists." & vbCrLf Dim HelpfulMessage As String = "Destination '" & DestinationFile HelpfulMessage += "' already exists" & vbCrLf Throw New IOException (HelpfulMessage, Exc) Catch Exc As PathTooLongException Dim HelpfulMessage As String = Exc.Message & vbCrLf HelpfulMessage += "Source (" & SourceFile.Length & ") = " _ & SourceFile & vbCrLf HelpfulMessage += "Destination (" & DestinationFile.Length & ") = " _ & DestinationFile & vbCrLf Throw New PathTooLongException(HelpfulMessage, Exc) Catch Exc As NotSupportedException Dim HelpfulMessage As String = "Either source or destination path" HelpfulMessage += " contains a colon" & vbCrLf HelpfulMessage += "Source = " & SourceFile & vbCrLf HelpfulMessage += "Destination = " & DestinationFile & vbCrLf Throw New NotSupportedException(HelpfulMessage, Exc) End Try End Sub
Notice in Listing 13-3 how the When clause is useful for filtering exceptions. With this clause, you can make your exception filters as powerful as you want. The only requirement is that the expression in the When clause must evaluate to a boolean ”in other words, a value of true or false . A drawback of using the When clause in the manner shown is that it can incur a performance hit if the expression takes some time to evaluate. This is a tradeoff that you have to consider against the benefit of improved exception messages. As you can see, the previous exception has now been wrapped by a more informative exception:
System.ArgumentException: Illegal characters in path. Source = C:\Demo\Source.txt Destination = C:\Demo\*.txt --> System.ArgumentException: Illegal characters in path. at System.Security.Permissions.FileIOPermission.HasIllegalCharacters(String str) at System.Security.Permissions.FileIOPermission.AddPathList(FileIOPermissionA ccess access, String pathList, Boolean checkForDuplicates, Boolean needFullPat h) at System.Security.Permissions.FileIOPermission..ctor(FileIOPermissionAccess access, String pathList, Boolean checkForDuplicates, Boolean needFullPath) at System.IO.File.Move(String sourceFileName, String destFileName) at ExceptionExperiments.Experiments.MoveFile(String SourceFile, String Destin ationFile) in C:\Documents and Settings\Administrator\My Documents\Visual Studio Projects\ExceptionExperiments\Experiments.vb:line 58 --- End of inner exception stack trace --- at ExceptionExperiments.Experiments.MoveFile(String SourceFile, String Destin ationFile) in C:\Documents and Settings\Administrator\My Documents\Visual Studio Projects\ExceptionExperiments\Experiments.vb:line 64 at ExceptionExperiments.Experiments.Main() in C:\Documents and Settings\Admin istrator\My Documents\Visual Studio Projects\ExceptionExperiments\Experiments.vb :line 13
Although this example shows how to improve on the exceptions produced by a Framework class by wrapping them, you can apply the same principle to exceptions produced by any third-party code.
When you use this technique, you should be aware of a nasty trap waiting for you, or at least for the callers of your code. Be careful to catch and throw only the most derived exception that you're looking to improve. For example, Listing 13-4 shows the wrong way to improve ArgumentException , by catching every ArgumentException , not just the specific exception that you want to improve. The problem is that ArgumentException is the base class for several other exceptions, and this code therefore makes the assumption that the caller never cares about the difference between these exceptions.
Sub MoveFile(ByVal SourceFile As String, ByVal DestinationFile As String) Try File.Move(SourceFile, DestinationFile) Catch Exc As ArgumentException Dim HelpfulMessage As String = Exc.Message & vbCrLf HelpfulMessage += "Source = " & SourceFile & vbCrLf HelpfulMessage += "Destination = " & DestinationFile Throw New ArgumentException(HelpfulMessage, Exc) 'Other Catch blocks go here End Try End Sub
Catch exception filters can't process inner exceptions, so the method shown in Listing 13-4 forces any calling code to catch the outer exception before it can analyze the type of the inner exception. This is the only way to distinguish between each of the possible exceptions that can derive from ArgumentException , and it results in very clumsy, slow, and error-prone code.
Sometimes you want to hide the implementation details of a method or improve the level of abstraction of a problem so that it's more meaningful to the caller of a method. To do this, you can intercept the original exception and substitute a different exception that's better suited for explaining the problem.
Listing 13-5 shows an example where a method loads the requested user's details from a text file. The method assumes that a text file exists named with the user's ID and a suffix of ".data". When that file doesn't actually exist, it doesn't make much sense to throw a FileNotFoundException because the fact that each user's details are stored in a text file is an implementation detail internal to the method. So this method instead wraps the original exception in a standard ArgumentException with an explanatory message. As I explained in the previous section, the original exception should be kept by loading it as the InnerException property of your new exception. This means that a developer can still analyze the underlying problem if necessary.
Function UserDetailsLoad(ByVal UserId As String) As Collection Dim HelpfulMessage As String Dim FileStream As System.IO.StreamReader 'First try to open the user's data file Try FileStream = New System.IO.StreamReader(UserId & ".data") Catch Exc As System.IO.FileNotFoundException 'The specified file didn't exist, but this exception won't mean 'much to a caller who doesn't know how this method is implemented UserDetailsLoad = Nothing HelpfulMessage = "Details for user " & UserId & " cannot be located" Throw New System.ArgumentException (HelpfulMessage, Exc) Finally End Try 'Code goes here to load and return user details End Function
Although wrapping an exception to make it more meaningful is a reasonable strategy, an even better one is to wrap the original exception in one of your own custom exceptions, as explained in the next section.
The problem with making an exception more meaningful by wrapping it in one of the standard Framework exceptions is that this technique often doesn't make the exception specific enough. Within an application, many different errors can result in an ArgumentException being thrown, so calling code still has to decipher more details about the exception in order to deal with it properly.
The best solution is to create a custom exception. This is similar to the VB.Classic approach of defining your own custom error numbers for specific situations. Custom exceptions are even better than custom error numbers because you can avoid error collisions by scoping your custom exceptions within each namespace, and you can provide whatever extended information you need by simply adding new exception properties. In addition, you can even create your own custom exception hierarchy for your component or application.
Listing 13-6 shows the same method as shown in Listing 13-5, but this time using a custom exception as the wrapper. Now there's no room for confusion in the calling code. If it catches the UserMissingDetailsException , there's no doubt about what has happened .
Function UserDetailsLoad(ByVal UserId As String) As Collection Dim HelpfulMessage As String Dim FileStream As System.IO.StreamReader 'First try to open the user's data file Try FileStream = New System.IO.StreamReader(UserId & ".data") Catch Exc As System.IO.FileNotFoundException 'The specified file didn't exist, but this exception won't mean 'much to a caller who doesn't know how this method is implemented UserDetailsLoad = Nothing HelpfulMessage = "Details for user " & UserId & " cannot be located" Throw New UserLoadException(HelpfulMessage, Exc, UserId) Finally End Try 'Code goes here to load and return user details End Function
You can build a custom exception by deriving it from System.ApplicationException . I discuss this in detail in the "Building Custom Exceptions" section later in this chapter.
There are three exceptions that need special handling by your application. This is because these three exceptions indicate that the CLR, the garbage collector, or your process is in deep trouble, and therefore that your application is rather unlikely to be able to recover from the exception.
ExecutionEngineException is thrown by the CLR when it detects an internal problem, such as a bug or data corruption. None of your Catch or Finally blocks are executed when this exception is thrown, and your process will be killed by the CLR unless a debugger can be found to attach to the process.
OutOfMemoryException is thrown when the CLR or the garbage collector can't find any free memory. If the garbage collector throws this exception, your Catch and Finally blocks will execute, but your application is unlikely to be able to create any new objects. If instead the CLR throws this exception, all of your code will be blown out of the water, and the CLR will terminate your process.
StackOverflowException is thrown by the CLR when either a CLR thread or an application thread has consumed all of its stack space. If a CLR thread causes this exception, you can't catch it and none of your Finally blocks will execute. Alternatively, you can catch this exception if it's thrown by one of your own application's threads, but none of your Finally blocks can execute without stack space. As the Finally blocks don't execute, your application is left in an undefined state, so you should never swallow this exception. Just log the exception in the Catch block, and let the CLR terminate your process.
If you try, you'll see that your own code can also throw these exceptions, and indeed any other exceptions. This isn't as daft as it sounds. The idea is that this allows you to test your exception-handling code in as many situations as possible.
There's another exception that can sometimes require special handling, namely ThreadAbortException . For details on how to deal with this exception, please see Chapter 14, which covers debugging multithreading situations.
There are three main situations where it's beneficial to specify your own custom exception. The first situation is when you want to signal an error or unexpected event that isn't described adequately by an existing exception. The second situation is when you're handling an exception raised by a component that you're calling, and you don't want to confuse the developer calling your component with an exception that's meaningless to that developer. The final situation is when you wish to wrap an existing exception to provide extra information.
As an example, I'm going to create the custom exception mentioned in Listing 13-6 and use this creation process to discuss how to build well-behaved custom exceptions.
The first task is to find a good name for a custom exception, something that conveys why the exception was thrown. I chose the name UserLoadException because this is going to be a base exception from which I can later derive more custom exceptions. Notice that the convention is that all exception names should end with the word "Exception".
Having arrived at a name, the next task is to decide which exception class is a suitable parent from which my custom exception can be inherited. Microsoft recommends that you should use System.ApplicationException as the parent of your base exceptions to differentiate them from the Framework class library exceptions, which mostly derive from System.SystemException .
This, however, is definitely not a hard-and-fast rule. For instance, in the method shown in Listing 13-6, you could consider deriving your exception from System.ArgumentException , or even directly from System.Exception . Deriving your custom exception from ArgumentException gives the benefit that any code already catching ArgumentException will also catch your new exception. Unfortunately, this is a dubious benefit because it's unlikely that pre-existing code will know how to handle your new exception. Deriving directly from Exception means that your new exception probably won't be caught by any pre-existing code, at least not until the application's unhandled exception handler is reached.
So deriving your base custom exceptions from ApplicationException is perhaps the best choice in most situations. Notice that Exception , ApplicationException , and SystemException all provide the same functionality, so you don't need to make a choice based on functionality.
Before you can use a custom exception, you obviously need to define constructors so that the exception can be created. The System.Exception base type defines three public constructors, so you should do the same. The first constructor should have no arguments; it can be used to create an instance of the exception with all properties set to default values. The second constructor should take a single string argument called message , which allows an exception message to be defined when the exception is created. The third constructor takes a string called message and an instance of an Exception -derived type. This second argument can be used to set the inner exception of the custom exception, as shown previously in Listing 13-6.
Notice that for these three constructors, each constructor can normally just call its base constructor to provide its functionality. Listing 13-7 shows the custom exception that I have defined so far.
Option Strict On Public Class UserLoadException : Inherits System.ApplicationException 'For new exception instance with default property values Public Sub New() MyBase.New() End Sub 'For new exception instance with specified message Public Sub New(ByVal message As String) MyBase.New(message) End Sub 'For new exception instance with specified message and inner exception Public Sub New(ByVal message As String, ByVal innerException As Exception) MyBase.New(message, innerException) End Sub End Class
This custom exception is already perfectly usable, but there are a couple of enhancements that can make it even more useful.
When an exception is passed across AppDomains or from one machine to another, it has to be deserialized and serialized before it can be used by the calling code. You can think of this as being similar to the Star Trek transporter process, where serialization is the equivalent of being sent (dematerialized at the sending end), and deserialization is similar to being received (rematerialized at the receiving end). To make sure that your custom exception can be marshaled in this manner, you need to make some additions to the code.
To make your custom exception serializable, you just need to add the Serializable attribute to the exception class. To enable the serializer to deserialize your exception properly, you must add a new deserialization constructor similar to the one that can be found in System.Exception . If your custom exception can be inherited, then this constructor should be defined as Protected , so that it's visible from any derived exception. If instead your exception is marked as NotInheritable , you should define the deserialization constructor as Private .
So with the addition of the serialization code, the custom exception now looks like Listing 13-8.
Option Strict On Imports System.Runtime.Serialization <Serializable()> _ Public Class UserLoadException : Inherits System.ApplicationException 'For new exception instance with default property values Public Sub New() MyBase.New() End Sub 'For new exception instance with specified message Public Sub New(ByVal message As String) MyBase.New(message) End Sub 'For new exception instance with specified message and inner exception Public Sub New(ByVal message As String, ByVal innerException As Exception) MyBase.New(message, innerException) End Sub 'To re-materialize exception at the receiving end Protected Sub New(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) MyBase.New(info, context) End Sub End Class
Now this custom exception can be marshaled across AppDomain and machine boundaries.
One of the benefits of custom exceptions is that you can add your own exception properties to help the consumers of your exceptions understand and handle them better. In this case, it would be beneficial to create an exception property that stores the UserId property, to avoid a developer having to store this somewhere or parse it out of the exception message. Listing 13-9 shows the custom exception with this additional property and now with two extra constructors so that the UserId property can be set when creating an instance of this exception.
Option Strict On Imports System.Runtime.Serialization <Serializable()> _ Public Class UserLoadException : Inherits System.ApplicationException 'Internal storage of the UserId property Private m_UserId As String = "" 'For new exception instance with default property values Public Sub New() MyBase.New() End Sub 'For new exception instance with specified message Public Sub New(ByVal message As String) MyBase.New(message) End Sub 'For new exception instance with specified message and UserId Public Sub New(ByVal message As String, ByVal UserId As String) MyBase.New(message) m_UserId = UserId End Sub 'For new exception instance with specified message and inner exception Public Sub New(ByVal message As String, ByVal innerException As Exception) MyBase.New(message, innerException) End Sub 'For new exception instance with specified message, inner exception and UserId Public Sub New(ByVal message As String, ByVal innerException As Exception, _ ByVal UserId As String) MyBase.New(message, innerException) m_UserId = UserId End Sub 'To re-materialize exception at the receiving end Protected Sub New(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) MyBase.New(info, context) End Sub 'UserId property, to help exception consumers ReadOnly Property UserId() As String Get UserId = m_UserId End Get End Property End Class
This custom exception is now looking fairly complete. The only remaining problem is that the new UserId property won't be serialized and deserialized automatically.
When you add custom properties to your custom exceptions, the serializer has to be told how to serialize and deserialize these properties. The first task is to serialize (in other words, dematerialize) the new UserId property. You do this by overriding the GetObjectData method that's called by the serializer to get the exception data, as shown in this code snippet:
'To serialize (de-materialize) custom property at the sending end Public Overrides Sub GetObjectData(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) MyBase.GetObjectData(info, context) info.AddValue("UserId", m_UserId) End Sub
The next task is to deserialize (in other words, rematerialize) the new UserId property at the receiving end. You do this by adding an extra line of code (shown in bold) to the previously implemented deserialization constructor, as shown in the following code snippet:
'To re-materialize exception and property at the receiving end Protected Sub New(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) MyBase.New(info, context) m_UserId = info.GetString("UserId") End Sub
The final task is to override and change the Message property so that it includes the value of the UserId property. This ensures that when a developer invokes ToString on the exception, he or she will see the exception message and the all-important user identifier.
'To add custom property to standard exception message Public Overrides ReadOnly Property Message() As String Get Return MyBase.Message & Environment.NewLine & "User id: " & m_UserId End Get End Property
Finally, after all this work, Listing 13-10 now shows the complete, ready-to-use, custom exception that I've created.
Option Strict On Imports System.Runtime.Serialization <Serializable()> _ Public Class UserLoadException : Inherits System.ApplicationException 'Internal storage of the UserId property Private m_UserId As String = "" 'For new exception instance with default property values Public Sub New() MyBase.New() End Sub 'For new exception instance with specified message Public Sub New(ByVal message As String) MyBase.New(message) End Sub 'For new exception instance with specified message and UserId Public Sub New(ByVal message As String, ByVal UserId As String) MyBase.New(message) m_UserId = UserId End Sub 'For new exception instance with specified message and inner exception Public Sub New(ByVal message As String, ByVal innerException As Exception) MyBase.New(message, innerException) End Sub 'For new exception instance with specified message, inner exception and UserId Public Sub New(ByVal message As String, ByVal innerException As Exception, _ ByVal UserId As String) MyBase.New(message, innerException) m_UserId = UserId End Sub 'To re-materialize exception and custom property at the receiving end Protected Sub New(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) MyBase.New(info, context) m_UserId = info.GetString("UserId") End Sub 'To de-materialize exception and custom property at the sending end Public Overrides Sub GetObjectData(ByVal info As SerializationInfo, _ ByVal context As StreamingContext) MyBase.GetObjectData(info, context) info.AddValue("UserId", m_UserId) End Sub 'To add custom property to standard exception message Public Overrides ReadOnly Property Message() As String Get Return MyBase.Message & Environment.NewLine & "User id: " & m_UserId End Get End Property 'UserId property, to help exception consumers ReadOnly Property UserId() As String Get UserId = m_UserId End Get End Property End Class
There's one final issue that you might have to deal with when you create custom exceptions. If you throw a custom exception across a machine or AppDomain (logical process) boundary, there's no guarantee that the component that receives the exception will be able to interpret it. An assembly will only know about custom exceptions that have been defined within its own AppDomain .
As an example, if your application is talking via remoting to a third-party application, and that application throws a custom exception, the CLR on your machine will throw a FileNotFoundException unless your application already knows about that custom exception. In a similar fashion, in the less likely situation of your AppDomain creating another AppDomain , any custom exception thrown by an assembly in the second AppDomain will cause a problem for your AppDomain unless the custom exception is defined in an application base common to both AppDomains .
There are two possible solutions to this issue. The first solution is to create an assembly containing the exception information and place that assembly into a common application base shared by both AppDomains . The second solution is to create the same assembly, sign it with a strong name and then place it in the global assembly cache (GAC). I usually prefer the first method because userdefined GAC assemblies can sometimes lead to policy configuration issues, but you should look carefully at your own application's needs before deciding which of these two methods makes the most sense in your situation.
To summarize, here's a list of the actions that you need to take when you build a custom exception:
Find a good name that conveys why the exception was thrown and make sure that the name ends with the word "Exception".
Ensure that you implement the three standard exception constructors.
Ensure that you mark your exception with the Serializable attribute.
Ensure that you implement the deserialization constructor.
Add any custom exception properties that might help developers to understand and handle your exception better.
If you add any custom properties, make sure that you implement and override GetObjectData to serialize your custom properties.
If you add any custom properties, override the Message property so that you can add your properties to the standard exception message.
The Visual Studio IDE lets you specify how the debugger should treat exceptions that are thrown while you're debugging within the IDE. You can access the dialog window that controls this by selecting Debug ’ Exceptions (see Figure 13-1).
The dialog window in Figure 13-1 shows the categories of exceptions that Visual Studio understands. If you haven't installed the C++ components of Visual Studio, you'll see only the Common Language Runtime Exceptions category. If you've installed C++, you'll see the following four exception categories:
C++ Exceptions: This option helps you work with unmanaged C++ exceptions.
Common Language Runtime Exceptions: This option helps you work with managed exceptions, including those thrown by VB .NET, C#, and Managed C++. Note that errors raised by VB.Classic components accessed through COM Interop are converted into CLR exceptions.
Native Run-Time Checks: This option is only useful if you're writing unmanaged C++ code.
Win32 Exceptions: This option is for working with exception codes thrown by unmanaged Win32 code.
I concentrate in this section on the CLR exceptions category, as these are by far the most common exceptions encountered by VB .NET developers. If you select this category, you can see that the exceptions are grouped by namespace. Selecting a namespace shows you a flat list of every exception within that namespace, as shown in Figure 13-2.
The two frames at the bottom of this dialog window allow you to control what happens when an exception is thrown and what happens when an exception is unhandled.
The first frame in Figure 13-2 contains three options for controlling what the debugger does as soon as an exception is thrown, before your application has a chance to handle it. These options apply to either an entire category of exceptions, the exceptions within a single namespace, or a single exception, depending upon which level of the exception hierarchy that you select in the Exceptions dialog window. The options for how the debugger treats an exception that's just been thrown are as follows :
Break into the debugger: This setting is most useful when you're debugging your exception-handling code. The debugger breaks execution at the line where the exception occurred and hands over control to you so that you can examine the exception and step through your Catch and Finally blocks. Alternatively, you can just continue from the break and exception handling will proceed normally. Exceptions that you select to treat this way are marked with a red ball glyph showing a white X.
Continue: This setting causes your component to continue normal execution after an exception, with the CLR looking locally and in the call stack for a catch exception filter that matches that exception. If the CLR finds an appropriate Catch block, your application continues normally. If no Catch block is found and therefore the exception is unhandled, the debugger will pause execution of your component and inform you about the unhandled exception. Exceptions that you select to treat this way are marked with a gray ball glyph.
Use parent setting: When this setting is selected for an individual exception within a namespace, it means the exception should be treated in the same way as its parent namespace. When this setting is selected for a complete namespace, it means the namespace should be treated in the same way as its parent namespace.
The "Use parent setting" option is useful for controlling debugger behavior for a whole group of exceptions simultaneously , but it isn't as useful as it first appears. Because this dialog window groups exceptions into namespaces rather than exception inheritance hierarchies, you can't specify that the debugger should treat derived exceptions like their base exception. To take an example, most developers aren't very interested in the debugger treating all of the exceptions within the System namespace in the same manner. Instead, they're more likely to want the debugger to handle the derived exceptions DivideByZeroException , NotFiniteNumberException , and OverflowException like their common base exception System.ArithmeticException . Unfortunately, this option isn't available.
The second frame in Figure 13-2 contains three options for controlling how the debugger deals with an unhandled exception ”one that doesn't match any Catch block exception filters in your application. As described previously, the options for dealing with unhandled exceptions apply to either an entire category of exceptions, the exceptions within a single namespace, or a single exception, depending upon which level of the exception hierarchy that you select in the exceptions dialog window. The options for how the debugger treats an unhandled exception are as follows:
Break into the debugger: When an exception is unhandled, this setting tells the debugger to break execution at the line where the exception occurred and hand over control to you. This is the default behavior, and I recommend always using this setting.
Continue: This setting causes the debugger to ignore unhandled exceptions, which means that the CLR will terminate your process when an unhandled exception occurs. This setting is useless for managed applications, but it will let your application continue executing after an exception occurs in, and stops, scripting code.
Use parent setting: As before, this tells the debugger to apply the setting you've defined for the parent node to the selected child node in the Exceptions window. This setting has the same drawback mentioned previously.
Please see the "Dealing with Unhandled Exceptions" section later in this chapter for a detailed look at how you can deal with unhandled exceptions in various types of VB .NET applications.
You can add your own exceptions to this dialog window. If you select the Common Language Runtime Exceptions node and click the Add button, you can enter the fully qualified name of a custom exception. The only slight caveat is that if you have two or more custom exceptions with the same name, but in different assemblies, this dialog window gives you no way of distinguishing between these exceptions. There's no validation that a custom exception with the name that you enter actually exists in your component. You should also be aware that the exception names are case-insensitive, so you can't define two exceptions whose name differs only by case.
You can also use the Clear All button to remove all user-defined exceptions from this dialog window, although there's no way to remove individual exceptions.
When a first-chance CLR exception is thrown, and you specify the "Break into the debugger" option for that exception, the debugger will break into your code at the line where the exception occurs, before any of your exception-handling code has had a chance to execute. You are then presented with the dialog window shown in Figure 13-3.
Clicking the Break button puts you on the line of code that caused the exception. Unfortunately, you can't get access to the exception itself at this point. If you look in the Output window, you can see the same exception message that the dialog window showed you, and if you look in the Call Stack window, you can see the exception call stack. Getting access to the other properties of the exception is not possible, but you can step from here into your exceptionhandling code for further debugging.
Clicking the Continue button lets the CLR continue execution of your code, so that it can attempt to find a Catch block with an exception filter matching the exception that was thrown. If the CLR doesn't find a suitable exception filter, it will terminate the thread in which the exception happened. If this was the main thread of your process, the process itself will be terminated . This is because, by design, the CLR is unable to continue after an unhandled exception on the main thread of a process.
One of the great benefits of SEH is that you can choose to handle the exceptions that you understand and just ignore the ones that you don't. These latter exceptions are then automatically propagated up the call stack for some other code to handle. But what happens if the CLR can't find a Catch block anywhere in the call stack with an exception filter that matches a specific exception?
Such an exception is called an unhandled exception. The problem with such an unhandled exception is that it can cause the CLR to terminate your application's process, or at least terminate the thread in which the exception occurred. This abrupt termination is usually a bad thing, because it's not what your end users want or expect, and it can result in corrupt or lost data. The fact that an exception wasn't handled just because it wasn't expected isn't usually a good reason for terminating your application. Normally, your application should terminate and roll back the task that was being performed when the exception occurred, but leave the application still running. This allows the end user to try some other way of performing the task or to continue with some other task.
Figure 13-4 shows the dialog window that the debugger produces when you opt to break into the debugger after an unhandled exception. At this point, clicking Break will cause the debugger to drop into the source code at the line that threw the exception, and clicking Continue will cause the process to terminate unless you have an unhandled exception filter in place. This is because the CLR can't continue properly after an unhandled exception in an application's main thread.
You can tell the CLR to notify you about all unhandled exceptions with just a few lines of code. Listing 13-11 shows a skeleton console application that has implemented an unhandled exception filter to catch all unhandled exceptions and report information about the exception and the CLR's intentions. The code shown constructs a delegate for System.UnhandledExceptionEventHandler at the start of the application, in this case at the beginning of the Main method. This delegate is registered with AppDomain s CurrentDomain.UnhandledException event. Whenever a managed thread has an unhandled exception, the CLR will invoke the UnhandledExceptionFilter method. Note that unhandled exceptions in unmanaged threads are ignored by the CLR.
Option Strict On Imports System Imports Microsoft.Win32 Module Startup Sub Main() 'Register my unhandled exception filter with the AppDomain. 'This means that my UnhandledExceptionFilter method will be 'called whenever an unhandled managed exception occurs. AddHandler AppDomain.CurrentDomain.UnhandledException, _ AddressOf UnhandledExceptionFilter 'Here's where the normal application code should appear. 'In this case, deliberately create an exception instead 'so that we can test the unhandled exception filter. Dim objTest As Object objTest.ToString() End Sub Private Sub UnhandledExceptionFilter(ByVal sender As Object, _ ByVal e As UnhandledExceptionEventArgs) 'This method will be called by any unhandled managed exception. 'It dumps relevant exception information to the console. 'Retrieve JIT debug setting from the registry. 'This can determine CLR behavior. Dim JitDebugSetting As Object Dim RegKey As RegistryKey = Registry.LocalMachine RegKey = RegKey.OpenSubKey("Software\Microsoft\ .NETFramework") JitDebugSetting = RegKey.GetValue("DbgJitDebugLaunchSetting") 'Debug or Release configuration? #If Debug Then Console.WriteLine("DEBUG configuration") #Else Console.WriteLine("RELEASE configuration") #End If 'Is a debugger attached to this process? Console.WriteLine("Debugger attached? " + Debugger.IsAttached.ToString) 'Does this application have a user interface? Console.WriteLine("End-user present? " + _ Environment.UserInteractive.ToString) 'Is this a CLS-compliant exception? Console.WriteLine("CLS-compliant exception? " _ + ((TypeOf e.ExceptionObject Is Exception).ToString)) 'What's the CLR going to do with the process? If e.IsTerminating = True Then Console.WriteLine("CLR will terminate this process") Else Console.WriteLine("CLR won't terminate this process") End If 'What's the CLR going to do about debugging? If Debugger.IsAttached = True Then Console.WriteLine("CLR didn't talk to user or spawn a debugger") Else 'If process is terminating, CLR checks registry for action. 'NB The CLR acted on this setting BEFORE this method was called! If e.IsTerminating = True Then If JitDebugSetting Is Nothing Then Console.WriteLine("No JIT debug setting in registry") Else Console.WriteLine("JIT debug setting: " + _ JitDebugSetting.ToString) Select Case JitDebugSetting Case 0 Console.WriteLine("CLR asked about starting debugger") Case 1 Console.WriteLine("CLR didn't ask or start debugger") Case 2 Console.WriteLine("CLR started debugger automatically") Case Else Console.WriteLine("JIT debug setting is invalid!") End Select End If Else Console.WriteLine("CLR didn't talk to user or spawn a debugger") End If End If 'Write exception to console Console.WriteLine(Environment.NewLine + "Exception text is:") Console.WriteLine(e.ExceptionObject.ToString) Console.ReadLine() End Sub End Module
One important concept to realize is that this method is considered by the CLR to be a catch filter, so that at this point in the process none of the Finally blocks between the method where the exception was thrown and this method at the top of the call stack have been executed. As soon as this unhandled exception filter has finished executing, any outstanding Finally blocks are in turn executed, starting at the bottom of the call stack and moving upward.
The unhandled exception filter in Listing 13-11 dumps some interesting information about the exception and the local environment to the console. If you execute this code under different application types and configurations, and with different exceptions, you will see the wide variety of situations that an unhandled exception filter has to deal with. When designing a realistic unhandled exception filter, you need to bear in mind several points:
Is this process running in Debug mode or Release mode?
Is a debugger attached to this process?
Does the application have a user interface?
Is the exception CLS-compliant?
Is the CLR going to terminate this process as a result of the exception?
What's the CLR going to do about debugging the process?
Apart from these factors, you also need to know whether you're dealing with aWindows Forms application, as the CLR exhibits some extra behavior in this case and you might want to intercept this behavior. Before going further, I'm going to discuss each of these points. Then I can put everything together and come up with something that tackles all of the possible scenarios.
The first and most important factor to consider is whether the CLR is going to terminate the process as a result of the exception. The CLR decides this by examining the type of thread that threw the exception.
If the thread was the finalizer thread, the CLR simply swallows the exception and moves on to call the next object's Finalize method. For any thread started manually with System.Threading.Thread , the CLR swallows the exception and kills the thread. For a thread in the thread pool, once again the exception is swallowed and then the thread is returned to the pool. In each of these cases, the CLR won't kill the process.
Only when the exception occurs on the main thread of a process does the CLR elect to kill the process. To reflect what's going to happen, the e.IsTerminating property is set to true or false .
Obviously, this factor will play some part in determining how you deal with the unhandled exception. If the process is going to terminate, you should definitely log this fact. If the application has a user interface (see the Environment.UserInteractive property), you might also want to produce a dialog window to inform the user about the exception and process termination, and maybe ask whether the user wants this exception reported automatically. This dialog window could perhaps offer to restart the application automatically and reload the user's work-in-progress. On the other hand, if the application is a Windows service or an XML Web service, displaying a user dialog would be futile. Instead, you need to warn any monitoring application about what's going to happen and maybe report the process death to the Windows event log.
In version 1.0 of the .NET Framework is a known bug that means the Environment.UserInteractive flag is always set to true . This bug has been fixed in version 1.1 of the .NET Framework.
If the process is going to be terminated as a result of the exception, you also need to deal with how the CLR makes choices about launching a debugger. The CLR first determines if a debugger is already attached to the process ”you can check this by using the System.Debugger.IsAttached property. If a debugger is attached, the CLR will let the process terminate without taking any further action, as soon as the unhandled exception filter has finished executing. If a debugger isn't already attached, the CLR uses the registry setting DbgJITDebugLaunchSetting , which exists under the HKEY_LOCAL_MACHINE section of the registry, as discussed in Chapter 3. This setting has three possible values, which have the following meanings:
0: The CLR displays the dialog window shown in Figure 13-5. If the user elects to debug the process, the CLR will launch a debugger using the command line specified in the DbgManagedDebugger registry subkey . If instead the user chooses not to debug the process, only then is your unhandled exception filter invoked followed by termination of the process.
Figure 13-5: The CLR asks the user whether he or she wants to debug an unhandled exception.
1: The CLR doesn't display a dialog window or launch a debugger. It just invokes your unhandled exception filter, followed by termination of the process.
2: Without displaying any dialog box, the CLR launches the debugger specified in the DbgManagedDebugger registry subkey and attaches it to the offending process. If the launched debugger is the Visual Studio debugger, you should remember that it has its own dialog window that asks the user how he or she wants to debug the application.
It's important to realize that when the CLR is going to terminate the process, all of this happens before your unhandled exception filter is invoked. If you want to alter this CLR debugging behavior, your application should arrange for this registry setting to be altered appropriately. The most flexible approach for any end user's production machine is probably to set this registry value to 1, so that you can control everything from your unhandled exception filter.
Alternatively, if the CLR isn't going to terminate the process as a result of the exception, you might consider launching a debugger yourself (using Debugger.Launch ), providing the application is running in Debug mode and a debugger isn't already attached.
The final factor to consider is whether the exception is CLS-compliant. In the majority of cases, this will always be true and you will have access to all of the standard exception properties. Otherwise, you only have access to the standard Object properties, such as ToString .
Pulling together all the information discussed in the previous section, I can start to put together a realistic unhandled exception filter that can cope with each of the possible scenarios. Listing 13-12 shows the same skeleton console application, but this time with a more sensible filter.
Option Strict On Imports System Imports Microsoft.Win32 Module Startup Sub Main() 'Register my unhandled exception filter with the AppDomain. 'This means that my UnhandledExceptionFilter method will be 'called whenever an unhandled managed exception occurs. AddHandler AppDomain.CurrentDomain.UnhandledException, _ AddressOf UnhandledExceptionFilter 'Here's where the normal application code should appear. 'In this case, deliberately create an exception instead 'so that we can test the unhandled exception filter. Dim objTest As Object objTest.ToString() End Sub Private Sub UnhandledExceptionFilter(ByVal sender As Object, _ ByVal e As UnhandledExceptionEventArgs) 'This method will be called by any unhandled managed exception Dim MessageFriendly As String, MessageDetail As String 'Log detailed exception message if no debugger is attached to this process If Debugger.IsAttached = False Then MessageDetail = "An unhandled exception occurred!" MessageDetail += Environment.NewLine MessageDetail += e.ExceptionObject.ToString MessageDetail += Environment.NewLine MessageDetail += "Application user was " + Environment.UserName MessageDetail += Environment.NewLine If e.IsTerminating = True Then MessageDetail += "CLR will terminate process" MessageDetail += " (exception was on main thread)." Else MessageDetail += "CLR won't terminate process" MessageDetail += " (exception was not on main thread)." End If 'Write detailed message to the Windows event log Dim EventLogWriter As New EventLog("Application", ".", _ System.AppDomain.CurrentDomain.FriendlyName) EventLogWriter.WriteEntry(MessageDetail, EventLogEntryType.Warning) EventLogWriter.Close EventLogWriter.Dispose End If #If Debug = True Then 'Launch the debugger if DEBUG build, no debugger already attached 'and this is an interactive process (user is present) If Environment.UserInteractive And (Debugger.IsAttached = False) Then Debugger.Launch() End If #Else 'If RELEASE build and this is an interactive process (user is present), 'then show user either a warning or a critical message If Environment.UserInteractive = True Then MessageFriendly = "Unfortunately, this application hit an problem." MessageFriendly += Environment.NewLine If e.IsTerminating = True Then MessageFriendly += "You can restart app after it has closed." MessageFriendly += Environment.NewLine MessageFriendly += "Please ask support team to examine event log." MsgBox(MessageFriendly, _ MsgBoxStyle.Critical Or MsgBoxStyle.OKOnly, _ "Sorry for the inconvenience") Else MessageFriendly += "Please save your work asap " MessageFriendly += "before restarting the application." MsgBox(MessageFriendly, _ MsgBoxStyle.Exclamation Or MsgBoxStyle.OKOnly, "Sorry for the inconvenience") End If End If #End If End Sub End Module
Just like in Listing 13-11, the console application shown in Listing 13-12 starts by constructing a delegate for System.UnhandledExceptionEventHandler . Once the unhandled exception filter has been called, it records a detailed exception message in the Windows event log, providing that no debugger is attached to the process. If a debugger is attached, it assumes that no exception logging will be needed.
After this, it will launch the debugger if the application is a debug build, no debugger is already attached and, most important, this is an interactive session. There is no point in launching a debugger automatically for an XML Web service or a Windows service.
If the application is instead a release build, and also only if it's an interactive process, the method displays either a message warning to the user to save his or her work if the process isn't going to terminate or a message telling the user that the application is going to close and referring the application support team to the detailed exception message in the Windows event log.
You should experiment and change this method to suit the exact requirements of your application. Perhaps you'll want to e-mail the exception message automatically and restart the application automatically. Or maybe you don't want to inform the user at all if the exception isn't going to result in process termination. There are many possibilities here, and you should think about these in the context of your own applications.
A Windows Form application adds yet another quirk to the process of dealing with unhandled exceptions. If a debugger is attached, everything will behave in the manner that you've already seen. However, if a debugger isn't attached, the CLR will by default show a warning dialog window to the end user similar to the one shown in Figure 13-6.
This dialog window is shown because an unhandled exception that occurs while a window message is processed ends up invoking Application.OnThreadException . Unless you override this method, it displays the dialog window shown in Figure 13-6. This dialog window informs the end user about the exception and asks whether the application should continue or quit. If the user does choose to continue, it's probably advisable for the user to save his or her work and quit the application as soon as possible.
After this dialog window is shown to the end user, the exception has been handled, so your unhandled exception filter discussed in the previous section won't be called. This means that the exception won't be logged unless the end user does so, which often means never! If this is a problem for you or for the support team, Listing 13-13 shows how you can override this default behavior in order to define your own behavior, which might include logging the unhandled exception and perhaps displaying your own "improved" dialog window.
Option Strict On Imports System Imports System.Windows.Forms Module UnhandledExceptions Sub Main() 'In a Windows Form app, if a managed exception occurs and a debugger 'isn't attached, the CLR will display an exception warning dialog. 'If we want to override this dialog, we need to register a delegate 'so that our method will be called instead. AddHandler Application.ThreadException, AddressOf OverrideClrDialog Here's where the normal application code should appear. 'In this case, the form will deliberately create an exception 'so that we can test the unhandled exception filter. Application.Run(New FormTest()) End Sub Private Sub OverrideClrDialog(ByVal sender As Object, _ ByVal e As System.Threading.ThreadExceptionEventArgs) This method will be called by any unhandled managed exception, 'assuming that a debugger wasn't attached. 'You should log the exception here, and maybe display your own dialog. MsgBox(e.Exception.ToString, MsgBoxStyle.OKOnly, "OverrideClrDialog") End Sub End Module
This will work fine for most unhandled exceptions, but you need to be aware that by doing this, you're ensuring that the CLR won't normally invoke your standard unhandled exception filter. This means that you should place all of the necessary code in your OverrideClrDialog method.
Instead of overriding the CLR dialog window, you can stop the CLR from displaying this window by setting the following line in the XML configuration file of your Windows Form application:
<configuration> <system.windows.forms jitDebugging="true" /> </configuration>
This line tells the CLR that you want a JIT debugger to deal with the unhandled exception rather than the CLR, and then your standard unhandled exception filter (or the JIT debugger) can catch the unhandled exception.
In some situations, you'll need to override the CLR dialog window in Windows Forms and still catch unhandled exceptions using a standard unhandled exception filter as discussed earlier. These situations occur because the Application.ThreadException event isn't invoked in any of the following circumstances:
An exception is thrown before the first window is launched.
An exception is thrown but not marshaled back to a window thread.
An exception is thrown while a debugger is attached.
The application configuration file specifies JIT debugging.
The exception isn't CLS-compliant.
Because of these situations, you may want to both override the CLR dialog window and also implement a standard unhandled exception filter. This is shown in Listing 13-14.
Option Strict On Imports System Imports System.Windows.Forms Module UnhandledExceptions Sub Main() 'First register the unhandled exception filter with the AppDomain. 'Even in a Windows Forms app, this is useful for certain situations: 'An exception is thrown before first window launched. 'If a debugger is attached, unhandled exception will still reach here. 'If app config sets JIT debugging, unhandled exception still reaches here. 'Exceptions that are not CLS-compliant will still only be caught here. AddHandler AppDomain.CurrentDomain.UnhandledException, _ AddressOf UnhandledExceptionFilter 'In a Windows Form app, if a managed exception occurs and a debugger 'isn't attached, the CLR will display an exception warning dialog. 'If we want to override this dialog, we need to register a delegate 'so that our method will be called instead. AddHandler Application.ThreadException, AddressOf OverrideClrDialog 'Here's where the normal application code should appear. 'In this case, the form will deliberately create an exception 'so that we can test the unhandled exception filter. Application.Run(New FormTest()) End Sub Private Sub UnhandledExceptionFilter(ByVal sender As Object, _ ByVal e As UnhandledExceptionEventArgs) 'This method deals with everything not handled by OverrideClrDialog MsgBox(e.ExceptionObject.ToString, MsgBoxStyle.OKOnly, _ "UnhandledExceptionFilter: " + e.IsTerminating.ToString) End Sub Private Sub OverrideClrDialog(ByVal sender As Object, _ ByVal e As System.Threading.ThreadExceptionEventArgs) 'This method will be called by any unhandled managed exception, 'assuming that a debugger wasn't attached. 'You should log the exception here, and maybe display your own dialog. MsgBox(e.Exception.ToString, MsgBoxStyle.OKOnly, "OverrideClrDialog") End Sub End Module
For internal applications running over an intranet, you shouldn't need to catch an unhandled exception inside of an XML Web service. This is because there will always be a client component waiting to catch most exceptions, and if the exception has reached the stage of being unhandled, it's likely that you won't know how to deal with that exception inside your XML Web service anyway.
For a Web service that interacts with external applications over the Internet, it's often a different story. Exposing every exception could involve significant security issues and allow malicious hackers to probe your Web service for weaknesses. In this case, you usually want to catch and record every unhandled exception and then throw a generic exception saying that the method call failed.
The first step is to realize that the Application_Error event in the global.asax file that you normally use to catch unhandled exceptions in ASP .NET doesn't work for unhandled exceptions within a Web service. This is because the HTTP handler for XML Web services catches any unhandled exception and turns it into a SOAP fault before the Application_Error event is called. This SOAP fault then becomes a SoapException or a SoapHeaderException , depending on whether the exception was thrown while processing a SOAP header or not. This exception is serialized and passed to the Web service's client. The SoapException (or SoapHeaderException ) provides no direct information about the original exception, although the text of the original exception is contained in the exception's Message property.
To catch an unhandled exception in a Web service, you need to build a SOAP extension to catch unhandled exceptions in a global exception handler. Within your SOAP extension, you can check for SOAP exceptions in the AfterSerialize stage of the ProcessMessage method. For full details on how to do this, please refer to Chapter 8, which covers the debugging of XML Web services.
The Exception Management Application Block is a free customizable exceptionhandling framework written by a team from Microsoft. This framework aims to make your application more robust, reduce the amount of custom errorhandling code that you have to write, and be flexible enough so that you can easily extend its functionality.
The framework comes as source code with considerable documentation, and it consists of two assemblies. The ExceptionManagement assembly contains the primary class, ExceptionManager , through which you publish exceptions. It also contains two other classes: DefaultPublisher for writing exception details to the Windows event log and ExceptionManagerInstaller for creating event log sources during installation. The ExceptionManagement.Interfaces assembly contains interfaces that are implemented by exception publisher classes, including optionally your own custom publishers.
To use the Exception Management Application Block from within your application, perform the following steps:
Build the two assemblies.
Set a reference in your project to the ExceptionManagement.dll assembly.
Add an Imports statement to reference the Microsoft.ApplicationBlocks.ExceptionManagement namespace.
Publish any exception by calling the static ExceptionManager.Publish method from within a Catch or Finally block, as shown in Listing 13-15.
Catch ex As Exception ExceptionManager.Publish(ex)
I have used this framework and recommend it highly. The two major benefits for me were the ability to configure the framework's behavior very easily using the standard .NET XML configuration files and the ease of creating and configuring custom exception publishers.
There are some caveats of which you should be aware. First, the Exception Management Application Block only works on Windows 2000 and Windows XP. Second, it only deals with exception publishing, so you still need to understand how to deal with unhandled exceptions and how to create custom exceptions. Finally, you need to come to grips with the copious documentation and understand how the framework works before implementing it in a production application.
If you run the Performance Monitor application as discussed in Chapter 6, you can see that .NET provides some performance counters specifically for measuring the effects that exceptions are having on your application. You can use these counters to monitor application performance and stability metrics, and to spot situations where an unusual number of exceptions might indicate that something is wrong.
Figure 13-7 shows the .NET exception performance counters displayed using Performance Monitor. If you don't see these counters when you run Performance Monitor, you may be a victim of a known bug where upgrading from .NET Beta 2 to RTM trashes some performance counters. To correct this, you should use the procedure described in the Microsoft Knowledge Base article Q306722.
Note that none of the "per second" exception performance counters are an average over time. Instead, each one displays the difference between the values produced by the last two samples divided by the number of seconds between the two sample times.
# of Exceps Thrown: This counter displays the total number of exceptions thrown since your application started running, including both handled and unhandled exceptions. It also includes exceptions that are rethrown. This counter isn't really of much use except as a general guideline.
# of Exceps Thrown / sec: This counter displays the number of exceptions thrown per second, including handled and unhandled exceptions. This is more useful than the previous counter, and you can use it to detect significant bursts of exceptions.
# of Filters / sec: This counter displays the number of Catch block exception filters evaluated each second. Note that this is the number of exception filters tried, not the number actually triggered. I can't see an interesting use for this performance counter, except maybe for a comparison of the same application executed several times.
# of Finallys / sec: This counter displays the number of Finally blocks executed each second as a result of exceptions being thrown. This number doesn't include Finally blocks executed on the normal code path. Once again, I can't see the use of this performance counter in most situations, except maybe as a general guideline.
Throw To Catch Depth / sec: This counter displays the number of stack frames (methods) traversed each second from where each exception is thrown to where it's caught. You could use this counter to understand variations in your application's performance, but it's probably more of a curiosity .
These performance counters are more useful for understanding subtleties in performance behavior rather than for diagnosing performance problems. They can be used to explain "why" rather than "what."
If you want to investigate some more code that demonstrates how to deal with exceptions, you can find a small example application in the Technologies\ Exceptions subfolder of the .NET Framework SDK samples folder.