- Threading in .NET
- Exception Handling in .NET
- The MSIL Disassembler
- From Here
Exception Handling in .NET
When writing code, one thing you can be sure of is that errors may occur. Typically you code will call Application Programming Interfaces (APIs) or other functions, and those functions may run into trouble. One approach for dealing with these errors is to monitor return values from functions to make sure that the intended result happened. The idea is to look for a successful return code and continue execution or detect that a failure has occurred and then handle the error condition correctly and gracefully terminate execution.
This works fine until you call a function or library that does not properly handle all possible error conditions. When this happens, the error can sometimes be passed back to your code and unexpected problems can occur. Exceptions provide a standardized way to generate and detect these errors before they can become a more serious problem in your application.
Exceptions allow you to build applications that make a call to another function and then catch errors if they occur and handle them in the calling application. However, exception handling differs from one programming language to another, if it is even supported at all! The Common Language Runtime solves this problem for all applications that use it. This section focuses on exception handling construction for Managed C++ application using the Common Language Runtime. The concepts are the same as traditional C++ exception handling, but the details vary a little.
Managed Exception Handling
Using exceptions in applications when you are using the Common Language Runtime provides the following benefits:
Exceptions can be handled regardless of the language that generated the exception or the one that is handling it.
Exceptions can be thrown (generated) from one process to another or even from a remote machine.
Exceptions can be thrown and caught in either managed or unmanaged code allowing you to work with exceptions outside of the environment that you are currently working in.
Exceptions in the Common Language Runtime do not require any special syntax but allows each language to define its own.
Exception handling, using the Common Language Runtime, is faster than using the Windows-based C++ error handling.
The Exception Class
At the center of exceptions in the Common Language Runtime is the Exception class. This base class is the class from which all other exception classes are derived. A portion of the hierarchy is shown in Figure 25.3. Two of the most common exception classes are SystemException and ApplicationException. Both of these classes inherit from Exception class but do not add any additional functionality. Why the two separate classes? It makes it easier to determine the origin and severity of the exception.
Figure 25.3 Examples of classes derived from the Exception class.
SystemException is used for exceptions that are generated by the CLR. The severity of the exception can be mild (such as a NULL reference exception) or more severe (such as a stack overflow exception.) SystemException is also the class from which ExternalException is derived. This allows the CLR to create managed exceptions that contain information about unmanaged exceptions. It is normally recommended that you do not catch exceptions of type SystemException. Let the runtime catch them, since the application is unlikely to be able to continue after the exception has occurred.
ApplicationException is the base class for exceptions that are thrown as part of an application. You can derive your own exceptions from the ApplicationException class. This is the exception type that you will most often catch in your application.
The Exception class contains several properties that can be examined when the exception has been caught. Table 25.3 lists the public properties and their description. The SystemException and ApplicationException classes inherit these properties.
Table 25.3 Properties of the Exception Class
Property Name |
Description |
HelpLink |
Contains a URL to more information about the exception. Set by the throwing function and may be blank |
InnerException |
Contains information about previous exceptions, if applicable. This can create a "stack" of exceptions that can show a chain of events |
Message |
Text that describes the details of the exception. Set by the throwing function. |
Source |
Specifies the object that threw the exception. |
StackTrace |
A string that show the function call stack at the time of the exception. |
TargetSite |
Name of the method that is throwing the exception. |
Throwing Exceptions in Managed Code
When an error occurs in your application, you should throw an exception to signal to the caller that an error has occurred. The way you do this in managed code is very simple, as demonstrated in Listing 25.4.
Listing 25.4 Throwing an Exception in Managed Code
#using<mscorlib.dll> using namespace System; int main() { Console::WriteLine(S"This is an example of throwing an exception"); throw new Exception(S"A sample exception"); return 0; }
This simple sample only throws the exception, using the throw statement. The string that is passed to the new Exception object is passed to the Message property of the object and provides information to the handler of the exception.
NOTE
If you create a solution and use the code from Listing 25.4, then execute it under the debugger, you will see a dialog box that says that an unhandled exception has occurred. This dialog also provides information about the exception, if available, and gives you the option of continuing with execution or breaking into the code where the exception is being handled. If you execute the code normally, the Just in Time debugger (JIT) dialog will appear and give you the option of debugging the failure.
Throwing an exception is easy. Handling the exception is also easy.
Try/Catch Block Construction in Managed Code
The Common Language Runtime exceptions use the same mechanism as the C++ language for exception handling, which is the Try/Catch statements. The use is very similar to the way as C++ exceptions. However, there are some minor differences.
Normally, code that might throw an exception is in a try/catch block. Perhaps the throw statement is in a function, and the call to the function is in a try/catch block. This enables you to catch any exceptions thrown by the function. To extend the sample above you could add a try and catch block as shown in Listing 25.5.
Listing 25.5 A General Try/Catch Block for Exception Handling
#using<mscorlib.dll> using namespace System; int main() { try { Console::WriteLine(S" This is an example of throwing an exception"); throw new Exception(S"A sample exception"); } catch(Exception* e) { Console::WriteLine(S"An exception has been caught: "); Console::WriteLine(e->ToString()); } return 0; }
The code in Listing 25.5 executes as the code of Listing 25.4 did, but now it traps the exception in the catch clause to examine the failure. First it writes to the screen that an exception has been caught and then it prints the important information in the exception object using the ToString method that is a member function of the Exception class.
In the example in Listing 25.5, the catch clause will catch any exception of the Exception object type. Normally, you construct a catch clause for a specific exception to handle the failure in our code and exit the error condition gracefully. For example, you passed a function a NULL parameter and the function did not support this then a System.ArgumentNullException would be generated.
try { DoesNotAcceptNULL(NULL); } catch(ArgumentNullException* e) { Console::WriteLine(S"A ArgumentNullException exception has been caught: "); Console::WriteLine(S"You should not pass NULL to this function"); }
The difference here is in the catch clause. This one catches only arguments of type ArgumentNullException[md]other exceptions will not be caught. You can catch several types of exceptions by coding several catch blocks, as in Listing 25.6.
Listing 25.6 Multiple Catch Blocks
try { DoesNotAcceptNULL(char* pChar); } catch(ArgumentNullException* e) { Console::WriteLine(S"A ArgumentNullException exception has been caught: "); Console::WriteLine(S"You should not pass NULL to this function"); } catch(Exception* e) { Console::WriteLine(S"An error has occurred, terminating application."); }
You can write catch blocks for each exception type you expect. If you catch exceptions in this manner, you will be able to tailor the response to an exception on a case by case basis. You can also have a general exception handler to handle the error cases that you do not need to do any special processing before exiting your code.
CAUTION
The order of the catch blocks is important. Catch clauses are checked in the order they appear in your code and so it is important to put catch blocks for specific exceptions before those for general. Failing to do this can cause unexpected results.
Using the __finally Clause
When an exception occurs, the control of the application is passed to the first exception handler that is found when the stack is unwound as it is with traditional C++ exceptions. After the handler has been executed, the function where the exception occurred is exited. Often you may have housekeeping code that should be executed, but that won't be executed after the exception handler has been invoked. This can result in a duplication of code within the catch block and at the end of the try block. The __finally mechanism ensures that your cleanup code is executed after the exception handler is done with its work.
The __finally block defines the code that we want to execute after the exception handler is finished. The code in the __finally block is always executed, whether an exception was thrown or not, making it a perfect place for housekeeping work to be done for the function. Listing 25.6 shows an example.
Listing 25.6 Using the __finally Block
#using<mscorlib.dll> using namespace System; int main() { int* buffer; try { int* buffer = new int[256]; } catch(OutOfMemoryException* e) { // Handle the Out of Memory error condition if allocation failed Console::WriteLine(S"Memory allocation failed!"); } __finally { // Do my normal housecleaning work here delete buffer; } }
CAUTION
Remember that any code that is placed into the __finally clause will be executed whether an exception was handled or not. Do not include error-handling code in the __finally block.