Base Class Exception Classes
This section provides a quick survey of some of the exceptions available in the .NET base class library. Microsoft has provided a large number of exception classes in .NET - too many to provide a comprehensive
The generic exception class, System.Exception, is derived from System.Object, as you would expect for a .NET class. In general, you should not throw generic System.Exception objects in your code, because they provide no specifics about the error condition.
Two important classes in the hierarchy are derived from System.Exception:
-
System.SystemException - This class is for exceptions that are usually thrown by the .NET runtime or that are considered to be of a generic nature and might be thrown by almost any application. For example, StackOverflowException will be thrown by the .NET runtime if it detects the stack is full. On the other hand, you might choose to throw ArgumentException or its subclasses in your own code, if you detect that a method has been called with inappropriate arguments. Subclasses of System.SystemException include classes that represent both fatal and nonfatal errors.
-
System.ApplicationException - This class is important, because it is the intended base for any class of exception defined by third parties. Hence, if you define any exceptions covering error conditions unique to your application, you should derive these directly or indirectly from System.ApplicationException.
Other exception classes that might come in handy include the following:
-
StackOverflowException - This exception is thrown when the area of memory allocated to the stack is full. A stack overflow can occur if a method continuously calls itself recursively. This is generally a fatal error, because it prevents your application from doing anything apart from terminating (in which case it is unlikely that even the finally block will execute). Trying to handle errors like this yourself is usually pointless and instead you should get the application to gracefully exit.
- OverflowException - An OverflowException is what happens if you attempt to cast an int containing a value of -40 to a uint in a checked context.
The class hierarchy for exceptions is somewhat unusual in that most of these classes do not add any functionality to their respective base classes. However, in the case of exception handling, the common reason for adding inherited classes is to indicate more specific error conditions, and there is often no need to override methods or add any new ones (although it is not uncommon to add extra properties that carry extra information about the error condition). For example, you might have a base ArgumentException class intended for method calls where inappropriate values are passed in, and an ArgumentNullException class derived from it, which is intended to handle a null argument if passed.
Catching Exceptions
Given that the .NET Framework includes a spate of predefined base class exception objects, how do you use them in your code to trap error conditions? In order to deal with possible error conditions in C# code, you will normally divide the relevant part of your program into blocks of three different types:
-
try blocks encapsulate the code that forms part of the normal operation of your program and that might encounter some serious error conditions.
-
catch blocks encapsulate the code that deals with the various error conditions that your code might have encountered by working through any of the code in the accompanying try block. This place could also be used for logging errors.
-
finally blocks encapsulate the code that cleans up any resources or takes any other action that you will normally want done at the end of a try or catch block. It is important to understand that the finally block is executed whether or not an exception is thrown. Because the aim is that the finally block contains cleanup code that should always be executed, the compiler will flag an error if you place a return statement inside a finally block. For an example of using the finally block, you might close any connections that were opened in the try block. It is also important to understand that the finally block is completely optional. If you don’t have a requirement for any cleanup code (such as disposing or closing any open objects), then there is no need for this block.
So how do these blocks fit together to trap error conditions? Here’s how:
-
The execution flow first enters the try block.
-
If no errors occur in the try block, execution proceeds normally through the block, and when the end of the try block is reached, the flow of execution jumps to the finally block if one is present (Step 5). However, if an error does occur within the try block, execution jumps to a catch block (next step).
-
The error condition is handled in the catch block.
-
At the end of the catch block, execution automatically transfers to the finally block if one is present.
-
The finally block is executed (if present).
The C# syntax used to bring all this about looks roughly like this:
Actually, a few variations on this theme exist:
-
You can omit the finally block because it is optional.
-
You can also supply as many catch blocks as you want to handle specific types of errors. However, the idea is that you shouldn’t get too carried away and have a huge number of catch blocks, as this can hurt the performance of your application.
-
You can omit the catch blocks altogether, in which case the syntax serves not to identify exceptions, but as a way of guaranteeing that code in the finally block will be executed when execution leaves the try block. This is useful if the try block contains several exit points.
So far so good, but the question that has yet to be answered is this: If the code is running in the try block, how does it know when to switch to the catch block if an error has occurred? If an error is detected, the code does something known as throwing an exception. In other words, it instantiates an exception object class and throws it:
Here, you have instantiated an exception object of the OverflowException class. As soon as the computer encounters a throw statement inside a try block, it immediately looks for the catch block associated with that try block. If there is more than one catch block associated with the try block, it identifies the correct catch block by checking which exception class the catch block is associated with. For example, when the OverflowException object is thrown, execution jumps to the following catch block:
In other words, the computer looks for the catch block that indicates a matching exception class instance of the same class (or of a base class).
With this extra information, you can expand the try block just demonstrated. Assume, for the sake of argument, that there are two possible serious errors that can occur in the try block: an overflow and an array out of bounds. Assume that your code contains two Boolean variables, Overflow and OutOfBounds, which indicate whether these conditions exist. You have already seen that a predefined exception class exists to indicate overflow (OverflowException); similarly, an IndexOutOfRangeException class exists to handle an array that is out of bounds.
Now your try block looks like this:
So far, this might not look that much different from what you could have done with the Visual Basic 6 On Error GoTo statement (with the exception perhaps that the different parts in the code are separated). C#, however, provides a far more powerful and flexible mechanism for error handling.
This is because you can have throw statements that are nested in several method calls inside the try block, but the same try block continues to apply even as execution flow enters these other methods. If the computer encounters a throw statement, it immediately goes back up through all the method calls on the stack, looking for the end of the containing try block and the start of the appropriate catch block. During this process, all the local variables in the intermediate method calls will correctly go out of scope. This makes the try...catch architecture well suited to the situation described at the beginning of this section, where the error occurs inside a method call that is nested inside 15 or 20 method calls, and processing has to stop immediately.
As you can probably gather from this discussion, try blocks can play a very significant part in controlling the flow of execution of your code. However, it is important to understand that exceptions are intended for exceptional conditions, hence their name. You wouldn’t want to use them as a way of controlling when to exit a do...while loop.
Implementing Multiple Catch Blocks
The easiest way to see how try...catch...finally blocks work in practice is with a couple of examples. The first example is called SimpleExceptions. It repeatedly asks the user to type in a number and then displays it. However, for the sake of this example, imagine that the number has to be between 0 and 5; otherwise, the program won’t be able to process the number properly. Therefore, you will throw an exception if the user types in anything outside of this range.
The program then continues to ask for more numbers for processing until the user simply presses the Enter key without entering anything.
|
Tip |
You should note that this code does not provide a good example of when to use exception handling. As already indicated, the idea of exceptions is that they are provided for exceptional circumstances. Users are always typing in silly things, so this situation doesn’t really count. Normally, your program will handle incorrect user input by performing an instant check and asking the user to retype the input if there is a problem. However, generating exceptional situations is difficult in a small example that you can read through in a few minutes! So, we will tolerate this bad practice for now in order to demonstrate how exceptions work. The examples that follow present more realistic situations. |
The code for SimpleExceptions looks like this:
The core of this code is a while loop, which continually uses Console.ReadLine() to ask for user input. ReadLine() returns a string, so your first task is to convert it to an int using the System.Convert .ToInt32() method. The System.Convert class contains various useful methods to perform data conversions and provides an alternative to the int.Parse() method. In general, System.Convert contains methods to perform various type conversions. Recall that the C# compiler resolves int to instances of the System.Int32 base class.
|
Tip |
It is also worth pointing out that the parameter passed to the catch block is scoped to that catch block - which is why you are able to use the same parameter name, ex, in successive catch blocks in the preceding code. |
In the preceding example, you also check for an empty string, because this is your condition for exiting the while loop. Notice how the break statement actually breaks right out of the enclosing try block as well as the while loop because this is valid behavior. Of course, once execution breaks out of the try block, the Console.WriteLine() statement in the finally block is executed. Although you just display a greeting here, more commonly, you will be doing tasks like closing file handles and calling the Dispose() method of various objects in order to perform any cleaning up. Once the computer leaves the finally block, it simply carries on executing unto the next statement that it would have executed had the finally block not been present. In the case of this example, though, you iterate back to the start of the while loop, and enter the try block once again (unless the finally block was entered as a result of executing the break statement in the while loop, in which case you simply exit the while loop).
Next, you check for your exception condition:
When throwing an exception, you need to choose what type of exception to throw. Although the class System.Exception is available, it is only intended as a base class. It is considered bad programming practice to throw an instance of this class as an exception, because it conveys no information about the nature of the error condition. Instead, the .NET Framework contains many other exception classes that are derived from System.Exception. Each of these matches a particular type of exception condition, and you are free to define your own ones as well. The idea is that you give as much information as possible about the particular exception condition by throwing an instance of a class that matches the particular error condition. In the preceding example, System.IndexOutOfRangeException is the best choice in the circumstances. IndexOutOfRangeException has several constructor overloads. The one chosen in the example takes a string, which describes the error. Alternatively, you might choose to derive your own custom Exception object that describes the error condition in the context of your application.
Suppose that the user then types a number that is not between 0 and 5. This will be picked up by the if statement and an IndexOutOfRangeException object will be instantiated and thrown. At this point, the computer will immediately exit the try block and hunt for a catch block that handles IndexOutOfRangeException. The first catch block it encounters is this:
Because this catch block takes a parameter of the appropriate class, the catch block will be passed the exception instance and executed. In this case, you display an error message and the Exception.Message property (which corresponds to the string you passed to the IndexOutOfRange’s constructor). After executing this catch block, control then switches to the finally block, just as if no exception had occurred.
Notice that in the example, you have also provided another catch block:
This catch block would also be capable of handling an IndexOutOfRangeException if it weren’t for the fact that such exceptions will already have been caught by the previous catch block. A reference to a base class can also refer to any instances of classes derived from it, and all exceptions are derived from System.Exception. So why isn’t this catch block executed? The answer is that the computer executes only the first suitable catch block it finds from the list of available catch blocks. So why is this second catch block even here? Well, it is not only your code that is covered by the try block. Inside the block, you actually make three separate calls to methods in the System namespace (Console.ReadLine(), Console.Write(), and Convert.ToInt32()), and any of these methods might throw an exception.
If you type in something that’s not a number - say a or hello - the Convert.ToInt32() method will throw an exception of the class System.FormatException to indicate that the string passed into ToInt32() is not in a format that can be converted to an int. When this happens, the computer will trace back through the method calls, looking for a handler that can handle this exception. Your first catch block (the one that takes an IndexOutOfRangeException) won’t do. The computer then looks at the second catch block. This one will do because FormatException is derived from Exception, so a FormatException instance can be passed in as a parameter here.
The structure of the example is actually fairly typical of a situation with multiple catch blocks. You start off with catch blocks that are designed to trap very specific error conditions. Then, you finish with more general blocks that will cover any errors for which you have not written specific error handlers. Indeed, the order of the catch blocks is important. If you had written the previous two blocks in the opposite order, the code would not have compiled, because the second catch block is unreachable (the Exception catch block would catch all exceptions). Therefore, the uppermost catch blocks should be the most granular options available and ending with the most general options.
However, in the previous example, you have a third catch block listed in the code:
This is the most general catch block of all - it doesn’t take any parameter. The reason this catch block is here is to catch exceptions thrown by other code that isn’t written in C# or isn’t even managed code at all. You see, it is a requirement of the C# language that only instances of classes derived from System .Exception can be thrown as exceptions, but other languages might not have this restriction - C++, for example, allows any variable whatsoever to be thrown as an exception. If your code calls into libraries or assemblies that have been written in other languages, it might find that an exception has been thrown that is not derived from System.Exception, although in many cases, the .NET PInvoke mechanism will trap these exceptions and convert them into .NET Exception objects. However, there is not that much that this catch block can do, because you have no idea what class the exception might represent.
|
Tip |
For this particular example, there is no point in adding this catch-all catch handler. Doing this is useful if you are calling into some other libraries that are not .NET-aware and that might throw exceptions. However, it is included it in the example to illustrate the principle. |
Now that you have analyzed the code for the example, you can run it. The following output illustrates what happens with different inputs and demonstrates both the IndexOutOfRangeException and the FormatException being thrown: