Chapter 9. Some Exceptional Exceptions

In This Chapter

  • Handling errors via return codes

  • Using the exception mechanism instead of return codes

  • Plotting your exception-handling strategy

I know it's difficult to accept, but occasionally a method doesn't do what it's supposed to do. Even the ones I write — especially the ones I write — don't always do what they're supposed to. Users are notoriously unreliable as well. No sooner do you ask for an int than a user inputs a double. Sometimes, the method goes merrily along, blissfully ignorant that it is spewing out garbage. However, good programmers write their methods to anticipate problems and report them as they occur.

Note

I'm talking about runtime errors, not compile-time errors, which C# spits out when you try to build your program. Runtime errors occur when the program is running, not at compile time.

The C# exception mechanism is a means for reporting these errors in a way that the calling method can best understand and use to handle the problem. This mechanism has a lot of advantages over the ways that programmers handled errors in the, uh, good old days. Let's revisit yesteryear so that you can see.

This chapter walks you through the fundamentals of exception handling. You have a lot to digest here, so lean back in your old, beat-up recliner.

Using an Exceptional Error-Reporting Mechanism

C# introduces a completely different mechanism for capturing and handling errors: the exception. This mechanism is based on the keywords try, catch, throw, and finally. In outline form, it works like this: A method will try to execute a piece of code. If the code detects a problem, it will throw an error indication, which your code can catch, and no matter what happens, it finally executes a special block of code at the end, as shown in this snippet:

public class MyClass
{
  public void SomeMethod()
  {
    // Set up to catch an error.
    try
    {
      // Call a method or do something that could throw an exception.
      SomeOtherMethod();
      // . . . make whatever other calls you want . . .
    }
    catch(Exception e)
    {
      // Control passes here in the event of an error anywhere
      // within the try block.
      // The Exception object e describes the error in detail.
    }
    finally
    {
      // Clean up here: close files, release resources, etc.
      // This block runs even if an exception was caught.
    }
  }
  public void SomeOtherMethod()
  {
     // . . . error occurs somewhere within this method . . .
     // . . . and the exception bubbles up the call chain.
     throw new Exception("Description of error");
     // . . . method continues if throw didn't happen . . .
  }
}

Note

The combination of try, catch, and (possibly) finally is an exception handler.

The SomeMethod() method surrounds a section of code in a block labeled with the keyword try. Any method called within that block (or any method that it calls or on up the tree . . .) is considered to be within the try block. If you have a try block, you must have either a catch block or a finally block, or both.

Warning

A variable declared inside a try, catch, or finally block isn't accessible from outside the block. If you need access, declare the variable outside, before the block:

int aVariable;  // Declare aVariable outside the block.
try
{
  aVariable = 1;
  // Declare aString inside the block.
  string aString = aVariable.ToString(); // Use aVariable in block.
}
// aVariable is visible here; aString is not.

About try blocks

Think of using the try block as putting the C# runtime on alert. If an exception pops up while executing any code within this block, hang a lantern in the old church tower (one if by land, two if by sea — or, call 911).

Then, if any line of code in the try block throws an exception — or if any method called within that method throws an exception, or any method called by those methods does, and so on — try to catch it.

Potentially, a try block may "cover" a lot of code, including all methods called by its contents. Exceptions can percolate up (sometimes a long way) from the depths of the execution tree. I show you examples.

About catch blocks

A try block is usually followed immediately by the keyword catch, which is followed by the catch keyword's block. Control passes to the catch block in the event of an error anywhere within the try block. The argument to the catch block is an object of class Exception or, more likely, a subclass of Exception.

If your catch doesn't need to access any information from the exception object it catches, you can specify only the exception type:

catch(SomeException)  // No object specified here (no "Exception e")
{
  // Do something that doesn't require access to exception object.
}

However, a catch block doesn't have to have arguments: A bare catch catches any exception, equivalent to catch(Exception):

catch
{
}

I have a lot more to say about what goes inside catch blocks in two articles on csharp102.info: "Creating your own exception class" and "Responding to exceptions."

Note

Unlike a C++ exception, in which the object in the catch argument can be any arbitrary object, a C# exception requires that the catch argument be a class that derives from Exception. (The Exception class and its numerous predefined subclasses are defined in the System namespace. Book II, Chapter 10 covers namespaces.)

About finally blocks

A finally block, if you supply one, runs regardless of whether the try block throws an exception. The finally block is called after a successful try or after a catch. You can use finally even if you don't have a catch. Use the finally block to clean up before moving on so that files aren't left open. Examples appear in the two exception-related articles on csharp102.info.

A common use of finally is to clean up after the code in the try block, whether an exception occurs or not. So you often see code that looks like this:

try
{
  ...
}
finally
{
  // Clean up code, such as close a file opened in the try block.
}

In fact, you should use finally blocks liberally — only one per try.

Note

A method can have multiple try/catch handlers. You can even nest a try/catch inside a try, a try/catch inside a catch, or a try/catch inside a finally — or all of the above. (And you can substitute try/finally for all of the above.) See the discussion of the using clause in Book II.

What happens when an exception is thrown

When an exception occurs, a variation of this sequence of events takes place:

  1. An exception is thrown. Somewhere deep in the bowels of SomeOtherMethod(), an error occurs. Always at the ready, the method reports a runtime error with the throw of an Exception object back to the first block that knows enough to catch and "handle" it.

    Note that because an exception is a runtime error, not a compile error, it occurs as the program executes. So an error can occur after you release your masterpiece to the public. Oops!

  2. C# "unwinds the call stack," looking for a catch block. The exception works its way back to the calling method, and then to the method that called that method, and so on, even all the way to the top of the program in Main() if no catch block is found to handle the exception. (We say more about unwinding the call stack, or call chain, in an article on csharp102.info. The article, which describes responding to an exception, explores your options.)

    Figure 9-1 shows the path that's followed as C# searches for an exception handler.

    Where, oh where, can a handler be found?

    Figure 9-1. Where, oh where, can a handler be found?

  3. If an appropriate catch block is found, it executes. An appropriate catch block is one that's looking for the right exception class (or any of its base classes). This catch block might do any of a number of things, which we cover in an article on csharp102.info. As the stack unwinds, if a given method doesn't have enough context — that is, doesn't know enough — to correct the exceptional condition, it simply doesn't provide a catch block for that exception. The right catch may be high up the stack.

    Note

    The exception mechanism beats the old-fashioned error-return mechanism described at the beginning of this chapter all hollow, for these reasons:

    • When the calling method gets an old-style return value and can't do anything useful, it must explicitly return the error itself to its caller, and so on. If the method that can handle the problem is far up the call chain, then returning a return that returned a return that . . . grows awkward, leading to some ugly design kludge. (Kludge is an engineer's term for something that works but is lame and ugly. Think "spit and baling wire.")

    • With exceptions, in contrast, the exception automatically climbs the call chain until it runs into an exception handler. You don't have to keep forwarding the message, which eliminates a lot of kludgy code.

  4. If a finally block accompanies the try block, it executes, whether an exception was caught or not. The finally is called before the stack unwinds to the next-higher method in the call chain. All finally blocks anywhere up the call chain also execute.

  5. If no catch block is found anywhere, the program crashes. If C# gets to Main() and doesn't find a catch block there, the user sees an "unhandled exception" message and the program exits. This is a crash. However, you can deal with exceptions not caught elsewhere by using an exception handler in Main(). See the section "Grabbing Your Last Chance to Catch an Exception," later in this chapter.

This exception mechanism is undoubtedly more complex and difficult to handle than using error codes. You have to balance the increased difficulty against these considerations, as shown in Figure 9-1:

  • Exceptions provide a more "expressive" model — one that lets you express a wide variety of error-handling strategies.

  • An exception object carries far more information with it, thus aiding in debugging — far more than error codes ever could.

  • Exceptions lead to more readable code — and less code.

  • Exceptions are an integral part of C# rather than an ad hoc, tacked-on afterthought such as error-code schemes, no two of which are much alike. A consistent model promotes understanding.

Throwing Exceptions Yourself

If classes in the .NET class library can throw exceptions, so can you.

To throw an exception when you detect an error worthy of an exception, use the throw keyword:

throw new ArgumentException("Don't argue with me!");

You have as much right to throw things as anybody. Because the .NET class library has no awareness of your custom BadHairDayException, who will throw it but you?

Tip

If one of the .NET predefined exceptions fits your situation, throw it. But if none fits, you can invent your own custom exception class.

Note

.NET has some exception types that you should never throw or catch: StackOverflowException, OutOfMemoryException, ExecutionEngineException, and a few more advanced items related to working with non-.NET code. The system owns them.

Knowing What Exceptions Are For

Note

Software that can't complete what it set out to do should throw exceptions. If a method is supposed to process all of an array, for example, or read all of a file — and for some reason can't complete the job — it should throw an appropriate exception.

A method can fail at its task for various reasons: bad input values or unexpected conditions (such as a missing or smaller than expected file), for example. The task is incomplete or can't even be undertaken. If any of these conditions occurs in your methods, you should throw an exception.

Note

The overall point here is that whoever called the method needs to know that its task wasn't completed. Throwing an exception is almost always better than using any error-return code.

What the caller does with the exception depends on the nature and severity of the problem. Some problems are worse than others. We use the rest of this chapter — plus the article "Responding to an exception" on csharp102.info — to explore the caller's options when your method "throws."

Can I Get an Exceptional Example?

The following FactorialException program demonstrates the key elements of the exception mechanism:

Note

// FactorialException -- Create a factorial program that reports illegal
//    Factorial() arguments using an Exception.
using System;

namespace FactorialException
{
  // MyMathFunctions -- A collection of mathematical functions
  //    we created (it's not much to look at yet)
  public class MyMathFunctions
  {
    // Factorial -- Return the factorial of the provided value.
    public static int Factorial(int value)
    {
      // Don't allow negative numbers.
      if (value < 0)
      {
        // Report negative argument.
        string s = String.Format(
             "Illegal negative argument to Factorial {0}", value);

        throw new ArgumentException(s);
      }

      // Begin with an "accumulator" of 1.
      int factorial = 1;

      // Loop from value down to 1, each time multiplying
      // the previous accumulator value by the result.
      do
      {
        factorial *= value;
      } while(--value > 1);

      // Return the accumulated value.
      return factorial;
    }
  }

  public class Program
  {
    public static void Main(string[] args)
    {
// Here's the exception handler.
try
{
  // Call factorial in a loop from 6 down to −6.
        for (int i = 6; i > −6; i--)
        {
          // Calculate the factorial of the number.
          int factorial = MyMathFunctions.Factorial(i);

          // Display the result of each pass.
          Console.WriteLine("i = {0}, factorial = {1}",
                            i, factorial);
        }
      }
      catch(ArgumentException e)
      {
        // This is a "last-chance" exception handler -- the buck stops at Main().
        // Probably all you can do here is alert the user before quitting.
        Console.WriteLine("Fatal error:");
        // When you're ready to release the program, change this
        // output to something in plain English, preferably with guide-
        // lines for what to do about the problem.
        Console.WriteLine(e.ToString());
      }

      // Wait for user to acknowledge.
      Console.WriteLine("Press Enter to terminate...");
      Console.Read();
    }
  }
}

This "exceptional" version of Main() wraps almost its entire contents in a try block. The catch block at the end of Main() catches the ArgumentException object and uses its ToString() method to display most of the error information contained within the exception object in a single string.

Note

I chose to use ArgumentException here because it most accurately describes the problem: an unacceptable argument to Factorial().

Knowing what makes the example exceptional

The version of the Factorial() method in the preceding section includes the same check for a negative argument as the previous version. (The test for an integer is no longer relevant because we changed the Factorial() parameter and return types to int.) If its argument is negative, Factorial() can't continue, so it formats an error message that describes the problem, including the value it found to be offensive. Factorial() then bundles this information into a newly created ArgumentException object, which it throws back to the calling method.

Tip

I recommend running the program in the debugger to watch the exception occur in real time. (I tell you more about the debugger in Book IV.)

The output from this program appears as follows (we trimmed the error messages to make them more readable):

i = 6, factorial = 720
i = 5, factorial = 120
i = 4, factorial = 24
i = 3, factorial = 6
i = 2, factorial = 2
i = 1, factorial = 1
i = 0, factorial = 0
Fatal error:
System.ArgumentException: Illegal negative argument to Factorial −1
   at Factorial(Int32 value) in c:c#programsFactorialProgram.cs:line 21
   at FactorialException.Program.Main(String[] args) in c:c#programsFactorial
     Program.cs:line 49
Press Enter to terminate...

The first few lines display the actual factorial of the numbers 6 through 0. Attempting to calculate the factorial of −1 generates the message starting with Fatal error — that doesn't sound good.

The first line in the error message was formatted back in Factorial() itself. This line describes the nature of the problem, including the offending value of −1.

Tracing the stack

The remainder of the output is a stack trace. The first line of the stack trace describes where the exception was thrown. In this case, the exception was thrown in Factorial(int) — more specifically, Line 21 within the source file Program.cs. Factorial() was invoked in the method Main(string[]) on Line 50 within the same file. The stack trace stops with Main() because that's the module in which the exception was caught — end of stack trace.

You have to admit that this process is impressive — the message describes the problem and identifies the offending argument. The stack trace tells you where the exception was thrown and how the program got there. Using that information, you should be drawn to the problem like a tornado to a trailer park.

Tip

If you run the previous example and examine the stack trace it prints to the console, you see Main() at the bottom of the listing and deeper methods above it. The trace builds upward from Main(), so, technically, unwinding the call stack goes down the trace toward Main(). You should think of it the other way around, though: Callers are higher in the call chain (refer to Figure 9-1).

Tip

Returning geeky information such as the stack trace works just fine during development, but you would probably want real users to see more intelligible information. Still, you may want to write the stack trace to a log file somewhere.

Note

The versions of Factorial() that I describe earlier in this chapter use a nonrecursive algorithm, which uses a loop to calculate the factorial. For a recursive version, see the RecursiveFactorial example on the Web. A recursive method calls itself, possibly repeatedly until a stopping condition occurs. The recursive Factorial() calls itself repeatedly (recurses), stopping when the value that's passed in becomes negative. Recursion is the most common way to implement Factorial(). Caution: Make sure that the recursion will stop. You can compare the results of RecursiveFactorial with those of the less exotic NonrecursiveFactorial example, and the DeadlyRecursion example shows what happens if the recursion doesn't stop — it results in quite an unpleasant StackOverflowException.

Tip

While the program is running in the debugger, the stack trace is available in one of the Visual Studio debugger windows.

Assigning Multiple catch Blocks

I mention earlier in this chapter that you can define your own custom exception types. Suppose that you defined a CustomException class. (I describe this process in the article "Creating Your Own Exception Class," which you can find on csharp102.info.) Now consider the catch clause used here:

public void SomeMethod()
{
  try
  {
    SomeOtherMethod();
  }
  catch(CustomException ce)
  {
  }
}

What if SomeOtherMethod() had thrown a simple Exception or another non-CustomException type of exception? It would be like trying to catch a football with a baseball glove — the catch doesn't match the throw.

Note

Fortunately, C# enables the program to define numerous catch clauses, each designed for a different type of exception. Assuming that this is the right place to handle the other exceptions, you can tack on one after another.

Don't be alarmed by my use of the word numerous. In practice, you don't use many catch blocks in one place.

Note

Multiple catch clauses for different exception types must be lined up nose to tail after the try block. C# checks each catch block sequentially, comparing the object thrown with the catch clause's argument type, as shown in this chunk of code:

public void SomeMethod()
{
  try
  {
    SomeOtherMethod();
  }
  catch(CustomException ce)  // Most specific exception type
  {
    // All CustomException objects are caught here.
  } // You could insert other exception types between these two.
  catch(Exception e)         // Most general exception type
  {
    // All otherwise uncaught exceptions are caught here.
    // Not that you should always do so -- but when it makes sense ...
  }
}

Were SomeOtherMethod() to throw an Exception object, it would pass over the catch(CustomException) because an Exception isn't a type of CustomException. It would be caught by the next catch clause: the catch(Exception).

Warning

Always line up the catch clauses from most specific to most general. Never place the more general catch clause first, as in this fairly awful bit of code:

public void SomeMethod()
{
  try
  {
    SomeOtherMethod();
  }
  catch(Exception e)   // Most general first -- not good!
  {
    // All exceptions are caught here.
    // The dingo ate everything.
  }
  catch(CustomException ce)
  {
    // No exception ever gets this far, because it's
    // caught and consumed by the more general catch clause.
  }
}

The more general catch clause starves the catch clause that follows by intercepting any throw. The compiler alerts you to this error.

Note

Any class that inherits CustomException IS_A CustomException:

class MySpecialException : CustomException
{
  // . . . whatever .. .
}

Given the chance, a CustomException catch grabs a MySpecialException object like a frog nabs flies.

Planning Your Exception-Handling Strategy

It makes sense to have a plan for how your program will deal with errors. Choosing to use exceptions instead of error codes is just one choice to make.

Due to space limitations, we can't fully explore all the options you have in responding to exceptions. This overview — a set of guidelines and some crucial techniques — should get you well oriented. Refer to the article on csharp102.info to dig much deeper into the basic question "What can I do when code throws an exception?"

Some questions to guide your planning

Several questions should be on your mind as you develop your program:

  • What could go wrong? Ask this question about each bit of code you write.

  • If it does go wrong, can I fix it? If so, you may be able to recover from the problem, and the program may be able to continue. If not, you probably need to pack your bags and get out of town.

  • Does the problem put user data at risk? If so, you must do everything in your power to keep from losing or damaging that data. Knowingly releasing code that can mangle user data is akin to software malpractice.

  • Where should I put my exception handler for this problem? Trying to handle an exception in the method where it occurs may not be the best approach. Often, another method higher up in the chain of method calls has better information and may be able to do something more intelligent and useful with the exception. Put your try/catch there so that the try block surrounds the call that leads to the place where the exception can occur.

  • Which exceptions should I handle? Catch any exception that you can recover from somehow. Try hard to find a way to recover, as discussed in the article "Responding to an Exception" on csharp102.info. Then, during development and testing, the unhandled exceptions will reach the top of your program. Before you release the program to real users, fix the underlying causes of any exceptions that go unhandled — if you can. But sometimes an exception should require terminating the program prematurely because things are hopelessly fouled up.

  • What about exceptions that slip through the cracks and elude my handlers? The section "Grabbing Your Last Chance to Catch an Exception," later in this chapter, describes providing a "last-chance" exception handler to catch strays.

  • How robust (unbreakable) does my code need to be? If your code operates an air-traffic control system, it should be robust indeed. If it's just a little one-off utility, you can relax a bit.

Guidelines for code that handles errors well

You should keep the questions in the previous section in mind as you work. These guidelines may help too:

  • Protect the user's data at all costs. This is the Top Dog guideline. See the "For More Information" sidebar at the end of this chapter Also see the next bullet item.

  • Don't crash. Recover if you can, but be prepared to go down as gracefully as possible. Don't let your program just squeak out a cryptic, geeky message and go belly up. Gracefully means that you provide clear messages containing as much helpful information as possible before shutting down. Users truly hate crashes. But you probably knew that.

  • Don't let your program continue running if you can't recover from a problem. The program could be unstable or the user's data left in an inconsistent state. When all is most certainly lost, you can display a message and call System.Environment.FailFast() to terminate the program immediately rather than throw an exception. It isn't a crash — it's deliberate.

  • Treat class libraries differently from applications. In class libraries, let exceptions reach the caller, who is best equipped to decide how to deal with the problem. Don't keep the caller in the dark about problems. But in applications, handle any exceptions you can. Your goal is to keep the code running if possible and protect the user's data without putting a lot of inconsequential messages in her face.

  • Throw exceptions when, for any reason, a method can't complete its task. The caller needs to know about the problem. (The caller may be a method higher up the call stack in your code or a method in code by another developer using your code). If you check input values for validity before using them and they aren't valid — such as an unexpected null value — fix them and continue if you can. Otherwise, throw an exception.

  • This advice is contrary to statements you may see elsewhere, but it comes from Jeffrey Richter, one of the foremost experts on .NET and C# programming. Often you hear statements such as "Exceptions are for unexpected situations only — don't use them for problems that are likely to occur in the normal course of operations." That's not accurate. An exception is the .NET way to deal with most types of errors. You can sometimes use an error code or another approach — such as having a collection method return −1 for "item not found" — but not often. Heed the previous paragraph.

  • Try to write code that doesn't need to throw exceptions — and correct bugs when you find them — rather than rely on exceptions to patch it up. But use exceptions as your main method of reporting and handling errors.

  • In most cases, don't catch exceptions in a particular method unless you can handle them in a useful way, preferably by recovering from the error. Catching an exception that you can't handle is like catching a wasp in your bare hand. Now what? Most methods don't contain exception handlers.

  • Test your code thoroughly, especially for any category of bad input you can think of. Can your method handle negative input? Zero? A very large value? An empty string? A null value? What could the user do to cause an exception? What fallible resources, such as files, databases, or URLs, does your code use? See the two previous bullet paragraphs.

  • Find out how to write unit tests for your code. It's reasonably easy and lots of fun.

  • Catch the most specific exception you can. Don't write many catch blocks for high-level exception classes such as Exception or ApplicationException. You risk starving handlers higher up the chain.

  • Always put a last-chance exception handler block in Main() — or wherever the "top" of your program is (except in reusable class libraries). You can catch type Exception in this block. Catch and handle the ones you can and let the last-chance exception handler pick up any stragglers. (We explain last-chance handlers in the later section "Grabbing Your Last Chance to Catch an Exception.")

  • Don't use exceptions as part of the normal flow of execution. For example, don't throw an exception as a way to get out of a loop or exit a method.

  • Consider writing your own custom exception classes if they bring something to the table — such as more information to help in debugging or more meaningful error messages for users. We introduce custom exceptions in an article on csharp102.info.

The rest of this chapter (along with the articles on csharp102.info) gives you the tools needed to follow those guidelines. For more information, look up exception handling, design guidelines in the Help system, but be prepared for some technical reading.

Note

If a public method throws any exceptions that the caller may need to catch, those exceptions are part of your class's public interface. You need to document them, preferably with the XML documentation comments discussed in Book IV.

How to analyze a method for possible exceptions

In the following method, which is Step 1 in setting up exception handlers, consider which exceptions it can throw:

public string FixNamespaceLine(string line)
{
  const string COMPANY_PREFIX = "CMSCo";
  int spaceIndex = line.IndexOf(' '),
  int nameStart = GetNameStartAfterNamespaceKeyword(line, spaceIndex);
  string newline = string.Empty;
  newline = PlugInNamespaceCompanyQualifier(line, COMPANY_PREFIX, nameStart);
  return newline.Trim();
}

Given a C# file, this method is part of some code intended to find the namespace keyword in the file and insert a string representing a company name (one of ours) as a prefix on the namespace name. (See Book II, Chapter 10 for information about namespaces.) The following example illustrates where the namespace keyword is likely to be found in a C# file:

using System;
namespace SomeName
{
  // Code within the namespace . . .
}

The result of running the FixNamespaceLine() method on this type of file should convert the first line into the second:

namespace SomeName
namespace CmsCo.SomeName

The overall program reads .CS files. Then it steps through the lines one by one, feeding each one to the FixNamespaceLine() method. Given a line of code, the method calls String.IndexOf() to find the index of the namespace name (normally, 10). Then it calls GetNameStartAfterNamespaceKeyword() to locate the beginning of the namespace name. Finally, it calls another method, PlugInNamespaceCompanyQualifier() to plug the company name into the correct spot in the line, which it then returns. Much of the work is done by the subordinate methods.

First, even without knowing what this code is for or what the two called methods do, consider the input. The line argument could have at least one problem for the call to String.IndexOf(). If line is null, the IndexOf() call results in an ArgumentNullException. You can't call a method on a null object. Also, at first blush, will calling IndexOf() on an empty string work? It turns out that it will, so no exception occurs there, but what happens if you pass an empty line to one of those methods with the long names? We recommend adding, if warranted, a guard clause before the first line of code in FixNamespaceLine() — and at least checking for null:

if(String.IsNullOrEmpty(name))  // A handy string method
{
  return name; // You can get away with a reasonable return value here
               // instead of throwing an exception.
}

Second, after you're safely past the IndexOf() call, one of the two method calls can throw an exception, even with line carefully checked out first. If spaceIndex turns out to be −1 (not found) — as can happen because the line that's passed in doesn't usually contain a namespace keyword — passing it to the first method can be a problem. You can guard for that outcome, of course, like this:

if(spaceIndex > −1) ...

If spaceIndex is negative, the line doesn't contain the namespace keyword. That's not an error. You just skip that line by returning the original line and then move on to the next line. In any event, don't call the subordinate methods.

Method calls in your method require exploring each one to see which exceptions it can throw and then digging into any methods that those methods call, and so on, until you reach the bottom of this particular call chain.

With this possibility in mind, and given that FixNamespaceLine() needs additional bulletproofing guard clauses first, where might you put an exception handler?

You may be tempted to put most of FixNamespaceLine() in a try block. But you have to consider whether this the best place for it. This method is low-level, so it should just throw exceptions as needed — or just pass on any exceptions that occur in the methods it calls. We recommend looking up the call chain to see which method might be a good location for a handler.

As you move up the call chain, ask yourself the questions in the earlier section "Some questions to guide your planning." What would be the consequences if FixNamespaceLine() threw an exception? That depends on how its result is used higher up the chain. Also, how dire would the results need to be? If you can't "fix" the namespace line for the current file, does the user lose anything major? Maybe you can get away with an occasional unfixed file, in which case you might choose to "swallow" the exception at some level in the call chain and just notify the user of the unprocessed file. Or maybe not. You get the idea. We discuss these and other exception-handling options in the article "Responding to an Exception" on csharp102.info.

The moral is that correctly setting up exception handlers requires some analysis and thought.

Note

However, keep in mind that any method call can throw exceptions — for example, the application could run out of memory, or the assembly it's in might not be found and loaded. You can't do much about that.

How to find out which methods throw which exceptions

Tip

To find out whether calling a particular method in the .NET class libraries, such as String.IndexOf() — or even one of your own methods — can throw an exception, consider these guidelines:

  • Visual Studio provides immediate help with tooltips. When you hover the mouse pointer over a method name in the Visual Studio editor, a yellow tooltip window lists not only the method's parameters and return type but also the exceptions it can throw.

  • If you have used XML comments to comment your own methods, Visual Studio shows the information in those comments in its IntelliSense tool tips just as it does for .NET methods. If you documented the exceptions your method can throw (see the previous section), you see them in a tooltip. The article "Getting Help in Visual Studio" on csharp102.info shows how to use XML comments, and the FactorialException example illustrates documenting Factorial() with XML comments. Plug in the <exception> line inside your <summary> comment to make it show in the tooltip.

  • The Help files provide even more. When you look up a .NET method in Help, you find a list of exceptions that the method can throw, along with additional descriptions not provided via the yellow Visual Studio tooltip. To open the Help page for a given method, click the method name in your code and press F1. You can also supply similar help for your own classes and methods.

You should look at each of the exceptions you see listed, decide how likely it is to occur, and (if warranted for your program) guard against it using the techniques covered in the rest of this chapter.

Grabbing Your Last Chance to Catch an Exception

The FactorialException example in the earlier section "Can I Get an Exceptional Example?" wraps all of Main(), except for the final console calls, in an outer, "last-chance" exception handler.

Note

If you're writing an application, always sandwich the contents of Main() in a try block because Main() is the starting point for the program and thus the ending point as well. (If you're writing a class library intended for reuse, don't worry about unhandled exceptions — whoever is using your library needs to know about all exceptions, so let them bubble up through your methods.)

Any exception not caught somewhere else percolates up to Main(). This is your last opportunity to grab the error before it ends up back in Windows, where the error message is much harder to interpret and may frustrate — or scare the bejabbers out of — the program's user.

All the serious code in FactorialException's Main() is inside a try block. The associated catch block catches any exception whatsoever and outputs a message to the console, and the application exits.

This catch block serves to prevent hard crashes by intercepting all exceptions not handled elsewhere. And it's your chance to explain why the application is quitting.

Experiment. To see why you need this last-chance handler, deliberately throw an exception in a little program without handling it. You see what the user would see without your efforts to make the landing a bit softer.

Note

During development, you want to see exceptions that occur as you test the code, in their natural habitat — so you want all of the geekspeak. In the version you release, convert the programmerish details to normal English, display the message to the user, including, if possible, what he might do to run successfully next time, and exit stage right. Make this plain-English version of the exception handler one of the last chores you complete before you release your program into the wild.

Your last-chance handler should certainly log the exception information somehow, for later forensic analysis.

Tip

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

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