Chapter 9
IN THIS CHAPTER
Handling errors via return codes
Using the exception mechanism instead of return codes
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.
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.
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 …
}
}
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.
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.
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
block doesn’t have to have arguments: A bare catch
catch
catches any exception, equivalent to catch(Exception)
:
catch
{
}
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.
A finally
block, if you supply one, runs regardless of the following:
try
block throws an exceptionbreak
or continue
to jump out of the blockreturn
statement to exit the method containing the blockThe 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:
try
block encounters a StackOverflowException
, OutOfMemoryException
, or ExecutingEngineException
.finally
block appears.When an exception occurs, a variation of this sequence of events takes place:
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!
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.
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.
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.
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:
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?
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):
Factorial()
to handle.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
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.
It makes sense to have a plan for how your program will deal with errors, as described in the following sections.
Several questions should be on your mind as you develop your program:
try…catch
there so that the try
block surrounds the call that leads to the place where the exception can occur.You should keep the questions in the previous section in mind as you work. These guidelines may help, too:
System.Environment.FailFast()
to terminate the program immediately rather than throw an exception. It isn’t a crash — it’s deliberate.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.
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
blocks for high-level exception classes such as Exception
or ApplicationException
.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.)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/
.
<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.
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.
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.
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();