Looking into Errors and Exception Handling


No matter how good your coding is, your programs should be able to handle any possible errors that may occur. For example, in the middle of some complex processing your code may discover that it doesn’t have permission to read a file, or while it is sending network requests the network may go down. In such exceptional situations, it is not enough for a method to simply return an appropriate error code - there might be 15 or 20 nested method calls, so what you really want the program to do is jump back up through all those 15 or 20 calls in order to exit the task completely and take the appropriate counteractions. The C# language has very good facilities to handle this kind of situation, through the mechanism known as exception handling.

Tip 

Error-handling facilities in Visual Basic 6 are very restricted and essentially limited to the On Error GoTo statement. If you are coming from a Visual Basic 6 background, you will find C# exceptions open up a whole new world of error handling in your programs. On the other hand, Java and C++ developers will be familiar with the principle of exceptions because these languages handle errors in a similar way to C#. Developers using C++ are sometimes wary of exceptions because of possible C++ performance implications, but this is not the case in C#. Using exceptions in C# code in general does not adversely affect performance. Visual Basic 2005 developers will find that working with exceptions in C# is very similar using exceptions in Visual Basic (except for the syntax differences).

Exception Classes

In C#, an exception is an object created (or thrown) when a particular exceptional error condition occurs. This object contains information that should help track down the problem. Although you can create your own exception classes (and you will be doing so later), .NET provides you with many predefined exception classes.

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 list here. This class hierarchy diagram in Figure 13-1 shows but a few of these classes, to give you a sense of the general pattern.

image from book
Figure 13-1

All the classes in Figure 13-1 are part of the System namespace, with the exception of IOException and the classes derived from IOException, which are part of the namespace System.IO. The System.IO namespace deals with reading and writing data to files. In general, there is no specific namespace for exceptions. Exception classes should be placed in whatever namespace is appropriate to the classes that can generate them - hence IO-related exceptions are in the System.IO namespace, and you will find exception classes in quite a few of the base class namespaces.

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.

  • EndOfStreamException - The usual cause of an EndOfStreamException is an attempt to read past the end of a file. A stream represents a flow of data between data sources. Streams are covered in detail in Chapter 35, “Accessing the Internet.”

  • 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 other exception classes shown in Figure 13-1 are not discussed here.

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:

  1. The execution flow first enters the try block.

  2. 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).

  3. The error condition is handled in the catch block.

  4. At the end of the catch block, execution automatically transfers to the finally block if one is present.

  5. The finally block is executed (if present).

The C# syntax used to bring all this about looks roughly like this:

  try {    // code for normal execution } catch {    // error handling } finally {    // clean up } 

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:

  throw new OverflowException(); 

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:

  catch (OverflowException ex) {    // exception handling here } 

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:

  try {    // code for normal execution    if (Overflow == true)   {       throw new OverflowException();    }    // more processing    if (OutOfBounds == true)    {       throw new IndexOutOfRangeException();    }    // otherwise continue normal execution } catch (OverflowException ex) {    // error handling for the overflow error condition } catch (IndexOutOfRangeException ex) {    // error handling for the index out of range error condition } finally {    // clean up } 

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:

  using System; namespace Wrox.ProCSharp.AdvancedCSharp {    public class MainEntryPoint    {       public static void Main()       {          string userInput;          while (true)          {             try             {                Console.Write("Input a number between 0 and 5 " +                   "(or just hit return to exit)> ");                userInput = Console.ReadLine();                if (userInput == "")                {                   break;                }                int index = Convert.ToInt32(userInput);                if (index < 0 || index > 5)                {                   throw new IndexOutOfRangeException(                      "You typed in " + userInput);                }                Console.WriteLine("Your number was " + index);             }             catch (IndexOutOfRangeException ex)             {                Console.WriteLine("Exception: " +                   "Number should be between 0 and 5. {0}", ex.Message);             }             catch (Exception ex)             {                Console.WriteLine(                  "An exception was thrown. Message was: {0}", ex.Message);             }             catch             {                Console.WriteLine("Some other exception has occurred");             }             finally             {                Console.WriteLine("Thank you");             }          }       }    } } 

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:

 if (index < 0 || index > 5) {    throw new IndexOutOfRangeException("You typed in " + userInput); }

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:

 catch (IndexOutOfRangeException ex) {    Console.WriteLine(       "Exception: Number should be between 0 and 5. {0}", ex.Message); }

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:

 catch (Exception ex) {    Console.WriteLine("An exception was thrown. Message was: {0}", ex.Message); }

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:

 catch {    Console.WriteLine("Some other exception has occurred"); }

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:

 SimpleExceptions Input a number between 0 and 5 (or just hit return to exit)> 4 Your number was 4 Thank you Input a number between 0 and 5 (or just hit return to exit)> 0 Your number was 0 Thank you Input a number between 0 and 5 (or just hit return to exit)> 10 Exception: Number should be between 0 and 5. You typed in 10 Thank you Input a number between 0 and 5 (or just hit return to exit)> hello An exception was thrown. Message was: Input string was not in a correct format. Thank you Input a number between 0 and 5 (or just hit return to exit)> Thank you

Catching Exceptions from Other Code

The previous example demonstrated the handling of two exceptions. One of them, IndexOutOfRangeException, was thrown by your own code. The other, FormatException, was thrown from inside one of the base classes. It is very common for code in a library to throw an exception if it detects that some problem has occurred, or if one of the methods has been called inappropriately by being passed the wrong parameters. However, library code rarely attempts to catch exceptions; this is regarded as the responsibility of the client code.

Often, you will find that exceptions get thrown from the base class libraries while you are debugging. The process of debugging to some extent involves determining why exceptions have been thrown and removing the causes. Your aim should be to ensure that by the time the code is actually shipped, exceptions do occur only in very exceptional circumstances, and if at all possible, are handled in some appropriate way in your code.

System.Exception Properties

The example has only illustrated the use of the Message property of the exception object. However, a number of other properties are available in System.Exception, as shown in the following table.

Open table as spreadsheet

Property

Description

Data

This provides you with the ability to add key/value statements to the exception that can be used to supply extra information about the exception. This is a new property in the .NET Framework 2.0.

HelpLink

This is a link to a help file that provides more information about the exception.

InnerException

If this exception was thrown inside a catch block, then InnerException contains the exception object that sent the code into that catch block.

Message

This is text that describes the error condition.

Source

This is the name of the application or object that caused the exception.

StackTrace

This provides details of the method calls on the stack (to help track down the method that threw the exception).

TargetSite

This is a .NET reflection object that describes the method that threw the exception.

Of these properties, StackTrace and TargetSite are supplied automatically by the .NET runtime if a stack trace is available. Source will always be filled in by the .NET runtime as the name of the assembly in which the exception was raised (though you might want to modify the property in your code to give more specific information), whereas Data, Message, HelpLink, and InnerException must be filled in by the code that threw the exception, by setting these properties immediately before throwing the exception. For example, the code to throw an exception might look something like this:

  if (ErrorCondition == true) {    Exception myException = new ClassmyException("Help!!!!");    myException.Source = "My Application Name";    myException.HelpLink = "MyHelpFile.txt";    myException.Data["ErrorDate"] = DateTime.Now;    myException.Data.Add("AdditionalInfo", "Contact Bill from the Blue Team");    throw myException; } 

Here, ClassMyException is the name of the particular exception class you are throwing. Note that it is common practice for the names of all exception classes to end with Exception. Also note that the Data property is assigned in two possible ways.

What Happens If an Exception Isn’t Handled?

Sometimes an exception might be thrown, but there might not be a catch block in your code that is able to handle that kind of exception. The SimpleExceptions example can serve to illustrate this. Suppose, for example, that you omitted the FormatException and catch-all catch blocks, and only supplied the block that traps an IndexOutOfRangeException. In that circumstance, what would happen if a FormatException were thrown?

The answer is that the .NET runtime would catch it. Later in this section, you learn how you can nest try blocks, and in fact, there is already a nested try block behind the scenes in the example. The .NET runtime has effectively placed the entire program inside another huge try block - it does this for every .NET program. This try block has a catch handler that can catch any type of exception. If an exception occurs that your code doesn’t handle, the execution flow will simply pass right out of your program and be trapped by this catch block in the .NET runtime. However, the results of this probably won’t be what you want. What happens is that the execution of your code will be terminated promptly and the user will see a dialog box that complains that your code hasn’t handled the exception as well as providing any details about the exception the .NET runtime was able to retrieve. At least the exception will have been caught though! This is what actually happened earlier in Chapter 2, “C# Basics,” in the Vector example when the program threw an exception.

In general, if you are writing an executable, you should try to catch as many exceptions as you reasonably can and handle them in a sensible way. If you are writing a library, it is normally best not to handle exceptions (unless a particular exception represents something wrong in your code that you can handle), but instead, assume that the calling code will handle any errors it encounters. However, you may nevertheless want to catch any Microsoft-defined exceptions, so that you can throw your own exception objects that give more specific information to the client code.

Nested try Blocks

One nice feature of exceptions is that you can nest try blocks inside each other, like this:

  try {    // Point A    try    {       // Point B    }    catch    {       // Point C    }    finally    {       // clean up    }    // Point D } catch {    // error handling } finally {    // clean up } 

Although each try block is only accompanied by one catch block in this example, you could string several catch blocks together, too. This section takes a closer look at how nested try blocks work.

If an exception is thrown inside the outer try block but outside the inner try block (points A and D), the situation is no different from any of the scenarios you have seen before: either the exception is caught by the outer catch block and the outer finally block is executed, or the finally block is executed and the .NET runtime handles the exception.

If an exception is thrown in the inner try block (point B), and there is a suitable inner catch block to handle the exception, then again you are in familiar territory: the exception is handled there, and the inner finally block is executed before execution resumes inside the outer try block (at point D).

Now suppose that an exception occurs in the inner try block, but there isn’t a suitable inner catch block to handle it. This time, the inner finally block is executed as usual, but then the .NET runtime will have no choice but to leave the entire inner try block in order to search for a suitable exception handler. The next obvious place to look is in the outer catch block. If the system finds one here, that handler will be executed and then the outer finally block. If there is no suitable handler here, the search for one will go on. In this case, it means the outer finally block will be executed, and then, because there are no more catch blocks, control will be transferred to the .NET runtime. Note that at no point is the code beyond point D in the outer try block executed.

An even more interesting thing happens if an exception is thrown at point C. If the program is at point C, it must be already processing an exception that was thrown at point B. It is in fact quite legitimate to throw another exception from inside a catch block. In this case, the exception is treated as if it had been thrown by the outer try block, so flow of execution will immediately leave the inner catch block, and execute the inner finally block, before the system searches the outer catch block for a handler. Similarly, if an exception is thrown in the inner finally block, control will immediately be transferred to the best appropriate handler, with the search starting at the outer catch block.

Important 

It is perfectly legitimate to throw exceptions from catch and finally blocks.

Although the situation has been shown with just two try blocks, the same principles hold no matter how many try blocks you nest inside each other. At each stage, the .NET runtime will smoothly transfer control up through the try blocks, looking for an appropriate handler. At each stage, as control leaves a catch block, any cleanup code in the corresponding finally block (if present) will be executed, but no code outside any finally block will be run until the correct catch handler has been found and run.

You have now seen how having nested try blocks can work. The obvious next question is why would you want to do that? There are two reasons:

  • To modify the type of exception thrown

  • To enable different types of exception to be handled in different places in your code

Modifying the Type of Exception

Modifying the type of the exception can be useful when the original exception thrown does not adequately describe the problem. What typically happens is that something - possibly the .NET runtime - throws a fairly low-level exception that says something like an overflow occurred (OverflowException) or an argument passed to a method was incorrect (a class derived from ArgumentException). However, because of the context in which the exception occurred, you will know that this reveals some other underlying problem (for example, an overflow can only have happened at that point in your code because a file you have just read contained incorrect data). In that case, the most appropriate thing that your handler for the first exception can do is throw another exception that more accurately describes the problem, so that another catch block further along can deal with it more appropriately. In this case, it can also forward the original exception through a property implemented by System.Exception called InnerException. InnerException simply contains a reference to any other related exception that was thrown - in case the ultimate handler routine will need this extra information.

Of course, the situation also exists where an exception occurs inside a catch block. For example, you might normally read in some configuration file that contains detailed instructions for handling the error, and it might turn out that this file is not there.

Handling Different Exceptions in Different Places

The second reason for having nested try blocks is so that different types of exceptions can be handled at different locations in your code. A good example of this is if you have a loop where various exception conditions can occur. Some of these might be serious enough that you need to abandon the entire loop, while others might be less serious and simply require that you abandon that iteration and move on to the next iteration around the loop. You could achieve this by having one try block inside the loop, which handles the less serious error conditions, and an outer try block outside the loop, which handles the more serious error conditions. You see how this works in the next exceptions example.

User-Defined Exception Classes

You are now ready to look at a second example that illustrates exceptions. This example, called SolicitColdCall, contains two nested try blocks and also illustrates the practice of defining your own custom exception classes and throwing another exception from inside a try block.

This example assumes that a sales company wants to have additional customers in on its sales list. The company’s sales team is going to phone a list of people in order to invite them to become customers, a practice known in sales jargon as cold calling people. To this end, you have a text file available that contains the names of the people to be cold called. The file should be in a well-defined format in which the first line contains the number of people in the file and each subsequent line contains the name of the next person. In other words, a correctly formatted file of names might look like this:

 4 George Washington Benedict Arnold John Adams Thomas Jefferson

This version of cold-calling is designed to display the name of the person on the screen (perhaps for the salesperson to read). That’s why only names and not phone numbers of the individuals are contained in the file.

For this example, your program will ask the user for the name of the file and will then simply read it in and display the names of people. That sounds like a simple task, but even so, a couple of things can go wrong and require you to abandon the entire procedure:

  • The user might type the name of a file that doesn’t exist. This will be caught as a FileNotFound exception.

  • The file might not be in the correct format. There are two possible problems here. First, the first line of the file might not be an integer. Second, there might not be as many names in the file as the first line of the file indicates. In both cases, you want to trap this oddity as a custom exception that has been written specially for this purpose, ColdCallFileFormatException.

There is also something else that could go wrong that won’t cause you to abandon the entire process but will mean that you need to abandon that person and move on to the next person in the file (and hence this will need to be trapped by an inner try block). Some people are spies working for rival sales companies, and obviously, you wouldn’t want to let these people know what you are up to by accidentally phoning one of them. Your research has indicated that you can identify who the spies are because their names begin with B. Such people should have been screened out when the data file was first prepared, but just in case any have slipped through, you will need to check each name in the file and throw a SalesSpyFoundException if you detect a sales spy. This, of course, is another custom exception object.

Finally, you will implement this example by coding a class, ColdCallFileReader, which maintains the connection to the cold-call file and retrieves data from it. You will code this class in a very safe way, which means that its methods will all throw exceptions if they are called inappropriately; for example, if a method that will read a file is called before the file has even been opened. For this purpose, you will write another exception class, UnexpectedException.

Catching the User-Defined Exceptions

Let’s start with the Main() method of the SolicitColdCall sample, which catches your user-defined exceptions. Note that you will need to call up file-handling classes in the System.IO namespace as well as the System namespace.

  using System; using System.IO; namespace Wrox.ProCSharp.AdvancedCSharp {    class MainEntryPoint    {       static void Main()       {          string fileName;       Console.Write("Please type in the name of the file " +          "containing the names of the people to be cold called > ");       fileName = Console.ReadLine();       ColdCallFileReader peopleToRing = new ColdCallFileReader();        try       {          peopleToRing.Open(fileName);          for (int i=0 ; i<peopleToRing.NPeopleToRing; i++)          {             peopleToRing.ProcessNextPerson();          }          Console.WriteLine("All callers processed correctly");       }       catch(FileNotFoundException ex)       {          Console.WriteLine("The file {0} does not exist", fileName);       }       catch(ColdCallFileFormatException ex)       {          Console.WriteLine(        "The file {0} appears to have been corrupted", fileName);          Console.WriteLine("Details of problem are: {0}", ex.Message);          if (ex.InnerException != null)          {             Console.WriteLine(                "Inner exception was: {0}", ex.InnerException.Message);          }       }       catch(Exception ex)       {          Console.WriteLine("Exception occurred:\n" + ex.Message);       }       finally       {          peopleToRing.Dispose();       }       Console.ReadLine();    } } 

This code is a little more than a loop to process people from the file. You start off by asking the user for the name of the file. Then you instantiate an object of a class called ColdCallFileReader, which is defined shortly. The ColdCallFileReader class is the class that handles the file reading. Notice that you do this outside the initial try block - that’s because the variables that you instantiate here need to be available in the subsequent catch and finally blocks, and if you declared them inside the try block they’d go out of scope at the closing curly brace of the try block, which would not be a good thing.

In the try block, you open the file (using the ColdCallFileReader.Open() method) and loop over all the people in it. The ColdCallFileReader.ProcessNextPerson() method reads in and displays the name of the next person in the file, while the ColdCallFileReader.NPeopleToRing property tells you how many people should be in the file (obtained by reading the first line of the file).

There are three catch blocks, one for FileNotFoundException, one for ColdCallFileFormatException, and one to trap any other .NET exceptions.

In the case of a FileNotFoundException, you display a message to that effect. Notice that in this catch block, the exception instance is not actually used at all. This catch block is used to illustrate the user-friendliness of the application. Exception objects generally contain technical information that is useful for developers, but not the sort of stuff you want to show to your end users. So in this case, you create a simpler message of your own.

For the ColdCallFileFormatException handler, you have done the opposite, and illustrated how to give fuller technical information, including details of the inner exception, if one is present.

Finally, if you catch any other generic exceptions, you display a user-friendly message, instead of letting any such exceptions fall through to the .NET runtime. Note that you have chosen not to handle any other exceptions not derived from System.Exception, because you are not calling directly into non-.NET code.

The finally block is there to clean up resources. In this case, this means closing any open file - performed by the ColdCallFileReader.Dispose() method.

Throwing the User-Defined Exceptions

Now take a look at the definition of the class that handles the file reading and (potentially) throws your user-defined exceptions: ColdCallFileReader. Because this class maintains an external file connection, you will need to make sure that it is disposed of correctly in accordance with the principles laid down for the disposing of objects in Chapter 4, “Inheritance.” Hence, you derive this class from IDisposable.

First, you declare some variables:

  class ColdCallFileReader : IDisposable {    FileStream fs;    StreamReader sr;    uint nPeopleToRing;    bool isDisposed = false;    bool isOpen = false; 

FileStream and StreamReader, both in the System.IO namespace, are the base classes that you will use to read the file. FileStream allows you to connect to the file in the first place, whereas StreamReader is specially geared up to reading text files and implements a method, StreamReader(), which reads a line of text from a file. You look at StreamReader more closely in Chapter 24, which discusses file handling in depth.

The isDisposed field indicates whether the Dispose() method has been called. ColdCallFileReader is implemented so that once Dispose() has been called, it is not permitted to reopen connections and reuse the object. isOpen is also used for error checking - in this case, checking whether the StreamReader actually connects to an open file.

The process of opening the file and reading in that first line - the one that tells you how many people are in the file - is handled by the Open() method:

  public void Open(string fileName) {    if (isDisposed)       throw new ObjectDisposedException("peopleToRing");    fs = new FileStream(fileName, FileMode.Open);    sr = new StreamReader(fs);    try    {       string firstLine = sr.ReadLine();       nPeopleToRing = uint.Parse(firstLine);       isOpen = true;    }    catch (FormatException ex)    {       throw new ColdCallFileFormatException(          "First line isn\'t an integer", ex);    } } 

The first thing you do in this method (as with all other ColdCallFileReader methods) is check whether the client code has inappropriately called it after the object has been disposed of, and throw a predefined ObjectDisposedException object if that has occurred. The Open() method checks the isDisposed field to see whether Dispose() has already been called. Because calling Dispose() implies that the caller has now finished with this object, you regard it as an error to attempt to open a new file connection if Dispose() has been called.

Next, the method contains the first of two inner try blocks. The purpose of this one is to catch any errors resulting from the first line of the file not containing an integer. If that problem arises, the .NET runtime will throw a FormatException, which you trap and convert to a more meaningful exception that indicates there is actually a problem with the format of the cold-call file. Note that System .FormatException is there to indicate format problems with basic data types, not with files, and so is not a particularly useful exception to pass back to the calling routine in this case. The new exception thrown will be trapped by the outermost try block. Because no cleanup is needed here, there is no need for a finally block.

If everything is fine, you set the isOpen field to true to indicate that there is now a valid file connection from which data can be read.

The ProcessNextPerson() method also contains an inner try block:

  public void ProcessNextPerson() {    if (isDisposed)    {       throw new ObjectDisposedException("peopleToRing");    }    if (!isOpen)    {       throw new UnexpectedException(          "Attempted to access cold-call file that is not open");    }    try    {       string name;       name = sr.ReadLine();       if (name == null)          throw new ColdCallFileFormatException("Not enough names");       if (name[0] == 'B')       {          throw new SalesSpyFoundException(name);       }       Console.WriteLine(name);    }    catch(LandLineSpyFoundException ex)    {       Console.WriteLine(ex.Message);    }    finally    {    } } 

Two possible problems exist with the file here (assuming that there actually is an open file connection; the ProcessNextPerson() method checks this first). First, you might read in the next name and discover that it is a land-line spy. If that condition occurs, the exception is trapped by the first of the catch blocks in this method. Because that exception has been caught here, inside the loop, it means that execution can subsequently continue in the Main() method of the program, and the subsequent names in the file will continue to be processed.

A problem might also occur if you try to read the next name and discover that you have already reached the end of the file. The way that the StreamReader object’s ReadLine() method works is if it has gone past the end of the file, it doesn’t throw an exception, but simply returns null. So, if you find a null string, you know that the format of the file was incorrect because the number in the first line of the file indicated a larger number of names than were actually present in the file. If that happens, you throw a ColdCallFileFormatException, which will be caught by the outer exception handler (which will cause execution to terminate).

Once again, you don’t need a finally block here because there is no cleanup to do; however, this time an empty finally block is included, just to show that you can do so, if you want.

The example is nearly finished. You have just two more members of ColdCallFileReader to look at: the NPeopleToRing property, which returns the number of people supposed to be in the file, and the Dispose() method, which closes an open file. Notice that the Dispose() method just returns if it has already been called - this is the recommended way of implementing it. It also checks that there actually is a file stream to close before closing it. This example is shown here to illustrate defensive coding techniques, so that’s what you are doing!

  public uint NPeopleToRing {    get    {       if (isDisposed)       {          throw new ObjectDisposedException("peopleToRing");       }       if (!isOpen)       {          throw new UnexpectedException(             "Attempted to access cold-call file that is not open");       }       return nPeopleToRing;    } } public void Dispose() {    if (isDisposed)    {       return;    }    isDisposed = true;    isOpen = false;    if (fs != null)    {       fs.Close();       fs = null;    } } 

Defining the Exception Classes

Finally, you need to define your own three exception classes. Defining your own exception is quite easy, because there are rarely any extra methods to add. It is just a case of implementing a constructor to ensure that the base class constructor is called correctly. Here is the full implementation of SalesSpyFoundException:

  class SalesSpyFoundException : ApplicationException {    public SalesSpyFoundException(string spyName)       :   base("Sales spy found, with name " + spyName)    {    }    public SalesSpyFoundException(       string spyName, Exception innerException)       :   base(          "Sales spy found with name " + spyName, innerException)    {    } } 

Notice that it is derived from ApplicationException, as you would expect for a custom exception. In fact, if you’d been going about this even more formally, you would probably have put in an intermediate class, something like ColdCallFileException, derived from ApplicationException, and derived both of your exception classes from this class, just to make sure that the handling code has that extra-fine degree of control over which exception handler handles which exception. However, to keep the example simple, you won’t do that.

You have done one bit of processing in SalesSpyFoundException. You have assumed that the message passed into its constructor is just the name of the spy found, so you turn this string into a more meaningful error message. You have also provided two constructors, one that simply takes a message, and one that also takes an inner exception as a parameter. When defining your own exception classes, it is best to include, at a minimum, at least these two constructors (although you won’t actually be using the second SalesSpyFoundException constructor in this example).

Now for the ColdCallFileFormatException. This follows the same principles as the previous exception, except that you don’t do any processing on the message:

  class ColdCallFileFormatException : ApplicationException {    public ColdCallFileFormatException(string message)       :   base(message)    {    }    public ColdCallFileFormatException(       string message, Exception innerException)       :   base(message, innerException)    {    } } 

And finally, UnexpectedException, which looks much the same as ColdCallFileFormatException:

  class UnexpectedException : ApplicationException {    public UnexpectedException(string message)       :   base(message)    {    }    public UnexpectedException(string message, Exception innerException)       :   base(message, innerException)    {    } } 

Now you are ready to test the program. First, try the people.txt file whose contents you displayed earlier. This has four names (which match the number given in the first line of the file), including one spy. Then try the following people2.txt file, which has an obvious formatting error:

 49 George Washington Benedict Arnold John Adams Thomas Jefferson

Finally, try the example but specify the name of a file that does not exist, people3.txt, say. Running the program three times for the three file names gives these results:

 SolicitColdCall Please type in the name of the file containing the names of the people to be cold called > people.txt George Washington Sales spy found, with name Benedict Arnold John Adams Thomas Jefferson All callers processed correctly

 SolicitColdCall Please type in the name of the file containing the names of the people to be cold called > people2.txt George Washington Sales spy found, with name Benedict Arnold John Adams Thomas Jefferson The file people2.txt appears to have been corrupted. Details of the problem are: Not enough names

 SolicitColdCall Please type in the name of the file containing the names of the people to be cold called > people3.txt The file people3.txt does not exist.

In the end, this little application shows you a number of different ways in which you can handle the errors and exceptions that you might find in your own applications.




Professional C# 2005 with .NET 3.0
Professional C# 2005 with .NET 3.0
ISBN: 470124725
EAN: N/A
Year: 2007
Pages: 427

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net