Chapter 9

Some Exceptional Exceptions

IN THIS CHAPTER

Bullet Handling errors via return codes

Bullet Using the exception mechanism instead of return codes

Bullet Plotting your exception-handling strategy

It’s difficult to accept, but occasionally application code doesn’t do what it’s supposed to do, which results in an error. Users are notoriously unreliable as well. No sooner do you ask for an int than a user inputs a double, which also results in an error. Sometimes the code goes merrily along, blissfully ignorant that it is spewing out garbage. However, good programmers write their code to anticipate problems and report them as they occur.

Remember This chapter discusses 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. 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.

Remember You don't have to type the source code for this chapter manually. In fact, using the downloadable source is a lot easier. You can find the source for this chapter in the CSAIO4D2EBK01CH09 folder of the downloadable source. See the Introduction for details on how to find these source files.

Using an Exceptional Error-Reporting Mechanism

C# provides a specific 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 …
}
}

Remember 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 the call tree within the block is part of 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.
}
catch (Exception e)
{
// Exception processing code.
}

// aVariable is visible here; aString is not.

About try blocks

Think of using the try block as putting the C# runtime on alert. The keyword is basically saying that the runtime should try to execute the code with the idea that it might not work. Exceptions bubble up through the code until the exception encounters a catch block or the application ends.

Tip When working with a try block, make the code within it specific to a particular task or exception. Keep the try block as short as possible to reduce debugging time and to make it more likely that the code can recover from the exception. Use multiple short try blocks rather than trying to encompass all the code that could generate an exception within a single try block. Using a single large try block is akin to trying to eat an entire hero sandwich in a single bite: You might succeed, but you won't enjoy it, and there is always a mess to clean up later.

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 as shown here:

catch (Exception e)
{
// Display the error
Console.WriteLine(e.ToString());
}

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
{
}

Tip Make catch blocks as specific as possible to reduce the work required to handle them and to make recovery more likely. In addition, you can stack catch blocks to enable you to handle exceptions as specifically as possible, with the most specific exception listed first. For example, in the following catch blocks, the ArgumentOutOfRangeException is the most specific exception, followed by ArgumentException, and finally the generic Exception:

catch (ArgumentOutOfRangeException e)
{
…Some remediation code to try.
}
catch (ArgumentException e)
{
…Some more remediation code to try.
}
catch (Exception e)
{
…Some last ditch remediation code to try.
}

After C# finds an exception handler that will address the exception, it ignores the other catch blocks, which is why you need the most specific exception listed first.

About finally blocks

A finally block, if you supply one, runs regardless of the following:

  • Whether the try block throws an exception
  • The code uses a break or continue to jump out of the block
  • There is a return statement to exit the method containing the block

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. For example, you use the finally block to clean up before moving on so that files aren't left open. A common use of finally is to clean up after the code in the try block, regardless of whether an exception occurs. 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. A finally block won't execute under these conditions:

  • The program terminates because a fatal exception isn’t handled.
  • The try block encounters a StackOverflowException, OutOfMemoryException, or ExecutingEngineException.
  • An external application kills the process in which the finally block appears.

Technicalstuff 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 the above. (And you can substitute try/finally for all the above.)

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. Figure 9-1 shows the path that's followed as C# searches for an exception handler.

    Schematic illustration of showing 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 derived classes — Exception will handle any exception, but ArgumentOutOfRangeException will handle exceptions only when the argument is out of range). This catch block might do any of a number of things. 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 higher up the stack.

  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 appropriate catch block is found anywhere, the program generally crashes.

    If C# gets to Main() and doesn't find a catch block there, the user sees an “unhandled exception” message, and the program normally exits unless the underlying operating system can supply a fix (which is rare). 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.

The exception mechanism provides you with these benefits:

  • Exceptions provide an expressive model — one that lets you express a wide variety of error-handling strategies.
  • An exception object carries a lot of information with it, thus aiding in debugging.
  • Exceptions lead to more readable code because you can see what could go wrong at a glance — and less code because you spend less time trying to figure out how to fix errors.
  • Exceptions are consistent, and a consistent model promotes understanding.

Technicalstuff C# provides mechanisms to support other error-handling methods that you will encounter when working with the underlying operating system directly. This book doesn't discuss such techniques, so you see exception handling as the only error-handling method. However, you can read more about interacting with the underlying Win32 API at https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/desktop-to-uwp-enhance, which is an advanced programming technique best left to people with a desire to slowly drive themselves nuts in pursuit of they know not what.

Throwing Exceptions Yourself

If code supplied with C# 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 C# 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. Using a standard exception is always preferred because .NET already provides documentation for it and developers are used to using it. But if none fits, you can invent your own custom exception class.

Technicalstuff .NET has some exception types that you should never catch: StackOverflowException, OutOfMemoryException, ExecutionEngineException, and a few more advanced items related to working with non-.NET code. These exceptions represent a kind of ultimate failure. For example, if you're out of stack space, as indicated by StackOverflowException, you simply don’t have any memory to continue executing the program. Given that exception handling occurs on the stack, you don’t even have enough memory to continue with the exception handling. Likewise, the OutOfMemoryException defines a condition in which your application is out of heap space (used for reference variables). And if the exception engine, ExecutionEngineException, isn't working at all, there isn’t any point in continuing because you have no way to handle this error.

Can I Get an Exceptional Example?

It’s helpful to see exceptions at work, so this section shows you how to create a basic application that exercises them. A factorial calculation (https://www.mathsisfun.com/numbers/factorial.html) performs a whole number calculation starting with the highest number down to 1, such as 4 * 3 * 2 * 1, for an output of 24, which would be 4! (or four factorial). The FactorialException program begins with the MyMathFunctions class shown here that contains the Factorial() function, which calculates the factorial of the number you provide:

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 ArgumentOutOfRangeException(s);
}

// Check specifically for 0.
if (value == 0)
return 1;

// 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;

// Check for an overflow.
if (factorial == 0)
{
string s = String.Format(
"Input Number {0} Too Large!", value);
throw new OverflowException(s);
}
} while (--value > 1);

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

The code you see doesn't catch every possible error, but it does look for negative numbers as bad inputs. If it sees such an error, it throws the ArgumentOutOfRangeException. This exception is specific to this particular error because negative numbers are not within the numeric range of 0 and above (0! has a value of 1, so there is a special check for it). You use this exception rather than ArgumentException because ArgumentOutOfRangeException is more specific than ArgumentException, which could refer to anything.

In addition, it's entirely possible that the user could enter a correct value but the Factorial() function won’t be able to handle it. In this case, the correct response is an OverflowException. The following code calls the Factorial() function:

static void Main(string[] args)
{
string Input = string.Empty;

while (Input.ToLower() != "quit")
{
// Get input from the user and check for Quit.
Console.WriteLine("Enter a positive number or Quit: ");
Input = Console.ReadLine();
if (Input.ToLower() == "quit")
continue;

// Make sure the input is an integer number.
int Value = 0;
if (!Int32.TryParse(Input, out Value))
{
Console.WriteLine("Please enter an integer number or Quit!");
continue;
}

// Here's the exception handler.
int Factorial = 0;
try
{
// Calculate the factorial of the number.
Factorial = MyMathFunctions.Factorial(Value);
}
catch (ArgumentOutOfRangeException e)
{
// Tell the user about the problem.
Console.WriteLine("You must enter a positive number!");

// Fix the problem by trying again.
continue;
}
catch (OverflowException e)
{
// Tell the user about the problem.
Console.WriteLine("The number supplied is too large!");

// Fix the problem by trying again.
continue;
}
catch (Exception e)
{
// OK, now we have no idea of what's wrong.
Console.WriteLine("An unexplainable error has happened!");

// Output the error information so someone can learn more.
Console.WriteLine(e.ToString());

// Exit the application gracefully with an error code.
System.Environment.Exit(-1);
}
finally
{
Console.WriteLine("Thank you for testing the application!");
}
Console.WriteLine($"The factorial of {Value} is {Factorial}.");
}

Console.Read();
}

This example takes a reasonable number of precautions. First, it checks to verify that the input is indeed an integer number or the special string quit (the capitalization is unimportant). Entering a character or floating-point value won’t do the trick. So, the application won’t even call factorial() unless there is a good chance of success.

Notice that the try…catch block is short and focuses on the call to Factorial(). The output doesn't appear until the try…catch block is satisfied. After the example calls Factorial(), it provides three levels of exception handling (in the following order):

  1. Display an error message because there is something wrong with the input, and give the user another chance to perform the task correctly.
  2. Handle the situation where the input is correct but the number is too high for Factorial() to handle.
  3. Do something when the unexplained happens to allow a graceful exit.

Because it's more likely that the user will provide incorrect input, rather than that Factorial() will receive too large a number, you check the incorrect input first. It’s a small but helpful way to improve application performance when an exception occurs. Here is a sample run of the code:

Enter a positive number or Quit:
S
Please enter an integer number or Quit!
Enter a positive number or Quit:
2.2
Please enter an integer number or Quit!
Enter a positive number or Quit:
-5
You must enter a positive number!
Thank you for testing the application!
Enter a positive number or Quit:
99
The number supplied is too large!
Thank you for testing the application!
Enter a positive number or Quit:
5
Thank you for testing the application!
The factorial of 5 is 120.
Enter a positive number or Quit:
quit

Working with Custom Exceptions

Earlier in the chapter, you learn that you can define your own custom exception types. Suppose that you want to define a CustomException class. The class might look something like this:

public class CustomException : System.Exception
{
// Default constructor
public CustomException() : base()
{
}

// Argument constructor
public CustomException(String message) : base(message)
{
}

// Argument constructor with inner exception
public CustomException(String message, Exception innerException) :
base(message, innerException)
{
}

// Argument constructor with serialization support
protected CustomException(
SerializationInfo info, StreamingContext context) :
base(info, context)
{
}
}

You can use this basic setup for any custom exception that you want to create. There is no special code (unless you want to add it) because the base() entries mean that the code relies on the code found in System.Exception. What you're seeing here is the work of inheritance, something you see quite a lot in Book 2. In other words, for now, you don’t have to worry too much about how this custom exception works.

Planning Your Exception-Handling Strategy

It makes sense to have a plan for how your program will deal with errors, as described in the following sections.

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.
  • 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? The answer is simple, all of them. There is never a good reason to leave exceptions unhandled in a way that leaves the user with one of those terrifying dialog boxes to gape at. If you can't provide a specific fix, at least exit from the application gracefully.
  • What about exceptions that slip through the cracks and elude my handlers? Testing, testing, and more testing is the word of the day! Exceptions should never just sort of happen to anyone.
  • What sort of input errors might my application see? Input errors happen from a wide range of sources, not just users. A file may contain bad data, or it might simply be missing or locked. Equipment, such as a network, used to retrieve data can malfunction. So, it’s not even enough to check for range and data type; you also need to check for issues such as missing sources.
  • How robust (unbreakable) does my code need to be? You never know where your code is going to end up. You might have designed it as a simple utility, but if it does something useful, it could end up in an air-traffic-control system. With this need in mind, you need to make your code as bulletproof as possible every time you write code.

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 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, which 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 the user’s 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.

    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 up the code. 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.
  • Catch the most specific exception you can. Don’t write many catch blocks for high-level exception classes such as Exception or ApplicationException.
  • 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. (The upcoming “Grabbing Your Last Chance to Catch an Exception” section explains last-chance handlers.)
  • 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. You can also use custom exception classes to catch a less informational exception, such as DivideByZero, and provide a more informational exception, such as InvalidSalesTaxRate, to pass to the caller.

The rest of this chapter gives you the tools needed to follow these guidelines. For more information check out https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/exceptions/.

Remember 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 “How to find out which methods throw which exceptions” section of Book 1, Chapter 9.

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 tooltip window lists not only the method’s parameters and return type but also the exceptions it can throw. If you need additional information, right-click the method name and choose Peek Definition from the context menu to see more specifics in a separate window.
  • If you have used XML comments to comment your own methods, Visual Studio shows the information in those comments in its IntelliSense tooltips 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, as part of autocomplete, and in the Object Browser. Place the <exception> line below your <summary> comment to make it show in the tooltip (see https://docs.microsoft.com/en-us/dotnet/csharp/codedoc for more details). Here is an example of XML comments for the Factorial() method in the FactorialException example:

    /// <summary>
    /// public static int Factorial(int value)
    /// <para>Return the factorial of the provided value.</para>
    /// <example>
    /// This shows a basic call.
    /// <code>
    /// input = 5;
    /// int output = Factorial(input);
    /// </code>
    /// </example>
    /// </summary>
    /// <param name="value">The x! to find.</param>
    /// <returns>Factorial value as int.</returns>
    /// <exception cref="ArgumentOutOfRangeException">
    /// The value must be greater than 0.
    /// </exception>
    /// <exception cref="OverflowException">
    /// The x! is too large to compute.
    /// </exception>

    When you hover your mouse over Factorial = MyMathFunctions.Factorial(Value);, you see the output shown in Figure 9-2.

  • The C# Language Help files provide even more. When you look up a .NET method in C# Language 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 C# Language 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.

Snapshot of providing XML comments for your methods.

FIGURE 9-2: Providing XML comments for your methods.

Grabbing Your Last Chance to Catch an Exception

Most applications benefit when you 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. 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 blocks catch any exception whatsoever and output a message to the console. In most cases, the application recovers from the error and gives the user another chance.

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

Remember During development, you want to see exceptions that occur as you test the code, in their natural habitat — so you want all the geekspeak. In the version you release, convert the programmerish details to normal English, display the message to the user, including, if possible, what the user 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.

Throwing Expressions

C# versions prior to 7.0 have certain limits when it comes to throwing an exception as part of an expression. In these previous versions, you essentially had two choices. The first choice was to complete the expression and then check for a result, as shown here:

var myStrings = "One,Two,Three".Split(',');
var numbers = (myStrings.Length > 0) ? myStrings : null;
if (numbers == null){ throw new Exception("There are no numbers!"); }

The second option was to make throwing the exception part of the expression, as shown here:

var numbers = (myStrings.Length > 0) ?
myStrings :
new Func<string[]>(() => {
throw new Exception("There are no numbers!"); })();

C# 7.0 and above includes a new null-coalescing operator, ?? (two question marks). Consequently, you can compress the two previous examples so that they look like this:

var numbers = myStrings ?? throw new Exception("There are no numbers!");

In this case, if myStrings is null, the code automatically throws an exception. You can also use this technique within a conditional operator (like the second example):

var numbers = (myStrings.Length > 0) ? myStrings :
throw new Exception("There are no numbers!");

The capability to throw expressions also exists with expression-bodied members. You might have seen these members in one of the two following forms (if not, you find them covered completely in Book 2, so don't worry too much about the specifics for now):

public string getMyString()
{
return " One,Two,Three ";
}

or

public string getMyString() => "One,Two,Three";

However, say that you don’t know what content to provide. In this case, you had these two options before version 7.0:

public string getMyString() => return null;

or

public string getMyString() { throw new NotImplementedException(); }

Both of these versions have problems. The first example leaves the caller without a positive idea of whether the method failed — a null return might be the expected value. The second version is cumbersome because you need to create a standard function just to throw the exception. Because of the new additions to C# 7.0 and above, it’s now possible to throw an expression in an expression-bodied method. The previous lines become

public string getMyString() => throw new NotImplementedException();

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

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