Chapter 16

Errors and Exceptions

WHAT’S IN THIS CHAPTER?

  • Looking at the exception classes
  • Using try . . . catch . . . finally to capture exceptions
  • Creating user-defined exceptions
  • Retrieving caller information

WROX.COM CODE DOWNLOADS FOR THIS CHAPTER

The wrox.com code downloads for this chapter are found at http://www.wrox.com/remtitle.cgi?isbn=1118314425 on the Download Code tab. The code for this chapter is divided into the following major examples:

  • Simple Exceptions
  • Solicit Cold Call
  • Caller Information

INTRODUCTION

Errors happen, and they are not always caused by the person who coded the application. Sometimes your application will generate an error because of an action that was initiated by the end user of the application, or it might be simply due to the environmental context in which your code is running. In any case, you should anticipate errors occurring in your applications and code accordingly.

The .NET Framework has enhanced the ways in which you deal with errors. C#’s mechanism for handling error conditions enables you to provide custom handling for each type of error condition, as well as to separate the code that identifies errors from the code that handles them.

No matter how good your coding is, your programs should be capable of handling any possible errors that may occur. For example, in the middle of some complex processing of your code, you 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 calls 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.

This chapter covers catching and throwing exceptions in many different scenarios. You will see exception types from different namespaces and their hierarchy, and learn about how to create custom exception types. You will learn different ways to catch exceptions, e.g. how to catch exceptions with the exact exception type or a base class. You will learn how to deal with nested try blocks, and how you could catch exceptions that way. For code that should be invoked no matter if an exception occurs or the code continues with any error, you will learn creating try/finally code blocks.

A new C# 5 feature that helps with handling errors enables the retrieval of caller information such as the file path, the line number, and the member name. This new feature is covered in the chapter as well.

By the end of this chapter, you will have a good grasp of advanced exception handling in your C# applications.

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 identify the problem. Although you can create your own exception classes (and you will be doing so later), .NET includes many predefined exception classes — too many to provide a comprehensive list here. The class hierarchy diagram in Figure 16-1 shows a few of these classes to give you a sense of the general pattern. This section provides a quick survey of some of the exceptions available in the .NET base class library.

All the classes in Figure 16-1 are part of the System namespace, except for IOException and CompositionException and the classes derived from these two classes. IOException and its derived classes are part of the namespace System.IO. The System.IO namespace deals with reading from and writing to files. CompositionException and its derived classes are part of the namespace System.ComponentModel.Composition. This namespace deals with dynamically loading parts and components. 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, I/O-related exceptions are in the System.IO namespace. 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:

  • 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 is thrown by the .NET runtime if it detects that the stack is full. However, 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 SystemException include classes that represent both fatal and nonfatal errors.
  • ApplicationException — With the initial design of the .NET Framework, this class was meant to be the base class for custom application exception classes. However, some exception classes that are thrown by the CLR derive from this base class (e.g., TargetInvocationException), and exceptions thrown from applications derive from SystemException (e.g., ArgumentException). Therefore, it’s no longer a good practice to derive custom exception types from ApplicationException, as this doesn’t offer any benefits. Instead, custom exception classes can derive directly from the Exception base class. Many exception classes in the .NET Framework directly derive from Exception.

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; instead, you should have the application 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 26, “Networking.”
  • OverflowException — An example when this occurs is 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 16-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. Often, it isn’t necessary 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 whereby 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 selection of predefined base class exception objects, this section describes how you use them in your code to trap error conditions. In dealing with possible error conditions in C# code, you will typically 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 dealing with the various error conditions that your code might have encountered by working through any of the code in the accompanying try block. This block could also be used for logging errors.
  • finally blocks encapsulate the code that cleans up any resources or takes any other action that you normally want handled 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 purpose of the finally block is to contain cleanup code that should always be executed, the compiler will flag an error if you place a return statement inside a finally block. An example of using the finally block is closing any connections that were opened in the try block. Understand that the finally block is completely optional. If your application does not require any cleanup code (such as disposing of or closing any open objects), then there is no need for this block.

The following steps outline how these blocks work together to trap error conditions:

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 (Step 3).
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, you don’t want to get too carried away and have a huge number of catch blocks.
  • You can omit the catch blocks altogether, in which case the syntax serves not to identify exceptions, but as a way to guarantee 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 occurs? 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 application encounters a throw statement inside a try block, it immediately looks for the catch block associated with that try block. If more than one catch block is 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 application 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 two possible serious errors can occur in the try block: an overflow and an array out of bounds. Assume also 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 a long time ago if you ever used the Visual Basic 6 On Error GoTo statement (with the possible exception that the different parts of 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 application 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, whereby 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 role in controlling the flow of your code’s execution. 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.


NOTE You should note that this code does not provide a good example of when to use exception handling, but it shows good practice on how to use exception handling. As their name suggests, exceptions are provided for other than normal circumstances. Users often type 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 it isn’t valid. However, generating exceptional situations is difficult in a small example that you can read through in a few minutes, so we will tolerate this less than ideal one to demonstrate how exceptions work. The examples that follow present more realistic situations.

The code for SimpleExceptions looks like this (code file SimpleExceptions/Program.cs):

using System;
        
namespace Wrox.ProCSharp.ErrorsAndExceptions
{
  public class Program
  {
    public static void Main()
    {
      while (true)
      {
        try
        {
          string userInput;
        
          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);
        }
        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 it 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.


NOTE 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, when 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 to perform any cleanup. After the application leaves the finally block, it simply carries on executing into 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 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 specify what type of exception to throw. Although the class System.Exception is available, it is intended only 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 as well. The goal is to provide 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 for the circumstances. IndexOutOfRangeException has several constructor overloads. The one chosen in the example takes a string describing 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 next 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 application 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 receive the exception instance and be executed. In this case, you display an error message and the Exception.Message property (which corresponds to the string passed to the IndexOutOfRangeException’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. This catch block isn’t executed because the application executes only the first suitable catch block it finds from the list of available catch blocks. This second catch block is here, however, because not only your own code 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 the user types in something that is 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 application 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) will not do. The application 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 with catch blocks that are designed to trap very specific error conditions. Then, you finish with more general blocks that cover any errors for which you have not written specific error handlers. Indeed, the order of the catch blocks is important. Had you 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, ending with the most general options.

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 demonstrates 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 a 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, exceptions are 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 occur only in very exceptional circumstances; and if possible, are handled appropriately in your code.

System.Exception Properties

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

PROPERTY DESCRIPTION
Data Enables you to add key/value statements to the exception that can be used to supply extra information about it
HelpLink 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 Text that describes the error condition
Source The name of the application or object that caused the exception
StackTrace Provides details about the method calls on the stack (to help track down the method that threw the exception)
TargetSite 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)
      {
        var 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. In addition, 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 is no 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 supplied only 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 does not 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 will not be what you want, as the execution of your code will be terminated promptly. The user will see a dialog that complains that your code has not handled the exception, and that provides any details about the exception the .NET runtime was able to retrieve. At least the exception will have been caught! This is what happened earlier in Chapter 2, “Core C#,” in the Vector example when the program threw an exception.

In general, if you are writing an executable, 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); 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 accompanied by only 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 a suitable inner catch block can 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 has no choice but to leave the entire inner try block 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, then that handler will be executed and then the outer finally block is executed. If there is no suitable handler here, the search for one continues. 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 the code beyond point D in the outer try block is not executed at any point.

An even more interesting thing happens when 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 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 immediately leaves the inner catch block, and executes 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 is immediately transferred to the best appropriate handler, with the search starting at the outer catch block.


NOTE It is perfectly legitimate to throw exceptions from catch and finally blocks. You can either just throw the same exception again using the throw keyword without passing any exception information, or throw a new exception object. Throwing a new exception you can assign the original exception with the constructor of the new object as inner exception. This is covered in “Modifying the Type of Exception” next.

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.

The nesting of try blocks can also occur between methods themselves. For example, if method A calls method B from within a try block, then method B itself has a try block within it as well.

Now that you have seen how having nested try blocks can work, let’s get into scenarios where this is very useful:

  • 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 indicating that something such as 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 happen at that point in your code because a file you 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, thereby enabling another catch block further along to deal with it more appropriately. In this case, it can also forward the original exception through a property implemented by Exception called InnerException, which simply contains a reference to any other related exception that was thrown — in case the ultimate handler routine needs this extra information.

Of course, an exception might occur inside a catch block. For example, you might normally read in a configuration file that contains detailed instructions for handling the error but it turns out that this file is not there.

Handling Different Exceptions in Different Places

The second reason to have 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 in which various exception conditions can occur. Some of these might be serious enough that you need to abandon the entire loop, whereas 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 a 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 will 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 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 increase its customer base. The company’s sales team is going to phone a list of people to invite them to become customers, a practice known in sales jargon as cold-calling. 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 is why only the names and not the 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 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 does not exist. This will be caught as a FileNotFound exception.
  • The file might not be in the correct format. There are two possible problems here. One, the first line of the file might not be an integer. Two, 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 especially for this purpose, ColdCallFileFormatException.

There is something else that can go wrong that, while not causing you to abandon the entire process, will mean you need to abandon a person’s name and move on to the next name in the file (and therefore trap it by an inner try block). Some people are spies working for rival sales companies, so you obviously do not want to let these people know what you are up to by accidentally phoning one of them. For simplicity, assume 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 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 reads 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 need to call up file-handling classes in the System.IO namespace as well as the System namespace (code file SolicitColdCall/Program.cs):

using System;
using System.IO;
        
namespace Wrox.ProCSharp.ErrorsAndExceptions
{
  class Program
  {
    static void Main()
    {
      Console.Write("Please type in the name of the file " +
          "containing the names of the people to be cold called > ");
      string fileName = Console.ReadLine();
      var 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)
      {
        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:
" + ex.Message);
      }
      finally
      {
        peopleToRing.Dispose();
      }
      Console.ReadLine();
    }
  }

This code is a little more than just a loop to process people from the file. You start 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 would go out of scope at the closing curly brace of the try block, where the compiler would complain about.

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, and the ColdCallFileReader.NPeopleToRing property indicates how many people should be in the file (obtained by reading the file’s first line). 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 end users. Therefore, in this case you create a simpler message of your own.

For the ColdCallFileFormatException handler, you have done the opposite, specifying how to obtain fuller technical information, including details about 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 here you are not handling 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, that means closing any open file — performed by the ColdCallFileReader.Dispose method.


NOTE C# offers a the using statement where the compiler itself creates a try/finally block calling the Dispose method in the finally block. The using statement is available on objects implementing a Dispose method. You can read the details of the using statement in Chapter 14.

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 need to ensure that it is disposed of correctly in accordance with the principles outlined for the disposing of objects in Chapter 4, “Inheritance.” Therefore, you derive this class from IDisposable.

First, you declare some private fields (code file SolicitColdCall/ColdCallFileReader.cs):

  public class ColdCallFileReader: IDisposable
  {
    private FileStream fs;
    private StreamReader sr;
    private uint nPeopleToRing;
    private bool isDisposed = false;
    private 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 enables you to connect to the file in the first place, whereas StreamReader is designed to read text files and implements a method, ReadLine, which reads a line of text from a file. You look at StreamReader more closely in Chapter 24, “Manipulating Files and the Registry,” which discusses file handling in depth.

The isDisposed field indicates whether the Dispose method has been called. ColdCallFileReader is implemented so that after 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 if so, throw a predefined ObjectDisposedException object. The Open method checks the isDisposed field to determine 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 throws a FormatException, which you trap and convert to a more meaningful exception that indicates 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, so it’s 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 coldcall 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(SalesSpyFoundException ex)
      {
        Console.WriteLine(ex.Message);
      }        
      finally
      {
      }
    }

Two possible problems exist with the file here (assuming there actually is an open file connection; the ProcessNextPerson method checks this first). One, you might read in the next name and discover that it is a sales spy. If that condition occurs, then the exception is trapped by the first catch block 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. Therefore, 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 causes the execution to terminate).

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 that are supposed to be in the file, and the Dispose method, which closes an open file. Notice that the Dispose method returns only if it has already been called — this is the recommended way of implementing it. It also confirms that there actually is a file stream to close before closing it. This example is shown here to illustrate defensive coding techniques:

    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 User-Defined 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 (code file SolicitColdCall/SalesSpyFoundException.cs):

  public class SalesSpyFoundException: Exception
  {
    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 Exception, as you would expect for a custom exception. In fact, in practice, you would probably have added an intermediate class, something like ColdCallFileException, derived from Exception, and then derived both of your exception classes from this class. This ensures that the handling code has that extra-fine degree of control over which exception handler handles each exception. However, to keep the example simple, you will not 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 will not actually be using the second SalesSpyFoundException constructor in this example).

Now for the ColdCallFileFormatException. This follows the same principles as the previous exception, but you don’t do any processing on the message (code file SolicitColdCall/ColdCallFileFormatException.cs):

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

Finally, UnexpectedException, which looks much the same as ColdCallFileFormatException (code file SolicitColdCall/UnexpectedException.cs):

 public class UnexpectedException: Exception
 {
   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. The contents are defined here:

4
George Washington
Benedict Arnold
John Adams
Thomas Jefferson

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, such as people3.txt. Running the program three times for the three filenames returns 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.

This application has demonstrated a number of different ways in which you can handle the errors and exceptions that you might find in your own applications.

CALLER INFORMATION

When dealing with errors, it is often helpful to get information about the error where it occurred. C# 5 has a new feature to get this information with the help of attributes and optional parameters. The attributes CallerLineNumber, CallerFilePath, and CallerMemberName, defined within the namespace System.Runtime.CompilerServices, can be applied to parameters. Normally with optional parameters, the compiler assigns the default values on method invocation in case these parameters are not supplied with the call information. With caller information attributes, the compiler doesn’t fill in the default values, but instead fills in the line number, file path, and member name.

The Log method from the following code snippet demonstrates how to use these attributes. With the implementation, the information is written to the console (code file CallerInformation/Program.cs):

    public void Log([CallerLineNumber] int line = -1,
      [CallerFilePath] string path = null, 
      [CallerMemberName] string name = null)
    {
      Console.WriteLine((line < 0) ? "No line" : "Line " + line);
      Console.WriteLine((path == null) ? "No file path" : path);
      Console.WriteLine((name == null) ? "No member name" : name);
      Console.WriteLine();
    }

Let’s invoke this method with some different scenarios. In the following Main method, the Log method is called by using an instance of the Program class, within the set accessor of the property, and within a lambda expression. Argument values are not assigned to the method, enabling the compiler to fill it in:

    static void Main()
    {
      var p = new Program();
      p.Log();
      p.SomeProperty = 33;
 
      Action a1 = () => p.Log();
      a1();
    }
 
    private int someProperty;
    public int SomeProperty
    {
      get { return someProperty; }
      set
      {
        this.Log();
        someProperty = value;
      }
    }

The result of the running program is shown next. Where the Log method was invoked, you can see the line numbers, the filename, and the caller member name. With the Log inside the Main method, the member name is Main. The invocation of the Log method inside the set accessor of the property SomeProperty shows SomeProperty. The Log method inside the lambda expression doesn’t show the name of the generated method, but instead the name of the method where the lambda expression was invoked (Main), which is of course more useful.

Line 11
c:ProCSharpErrorsAndExceptionsCallerInformationProgram.cs
Main
 
Line 24
c:ProCSharpErrorsAndExceptionsCallerInformationProgram.cs
SomeProperty
 
Line 14
c:ProCSharpErrorsAndExceptionsCallerInformationProgram.cs
Main

Using the Log method within a constructor, the caller member name shows ctor. With a destructor, the caller member name is Finalize, as this is the method name generated.


NOTE A great use of the CallerMemberName attribute is with the implementation of the interface INotifyPropertyChanged. This interface requires the name of the property to be passed with the method implementation. You can see the implementation of this interface in several chapters in this book — for example,Chapter 36, “Business Applications with WPF.’

SUMMARY

This chapter examined the rich mechanism C# provides for dealing with error conditions through exceptions. You are not limited to the generic error codes that could be output from your code; instead, you have the capability to go in and uniquely handle the most granular of error conditions. Sometimes these error conditions are provided to you through the .NET Framework itself; but at other times, you might want to code your own error conditions as illustrated in this chapter. In either case, you have many ways to protect the workflow of your applications from unnecessary and dangerous faults.

The next chapter enables you to implement a lot of what you learned so far in this book within the .NET developer’s IDE — Visual Studio 2012.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset