GETTING A PROGRAM TO WORK UNDER IDEAL circumstances is usually a lot easier than making the program robust. A robust program can survive unusual or "exceptional" circumstances without crashing. One approach to writing robust programs is to anticipate the problems that might arise and to include tests in the program for each possible problem. For example, think of a program that asks the user to enter an amount of currency, including the $ sign (if the amount is in dollars), or another symbol (if the amount is in some other currency). Because of the leading currency symbol, we must use the readLine() method to read the user's response. The program needs to extract the first character to determine what kind of currency the user entered, and then it needs to convert the rest of the String to a numeric value. Here's the basic idea:
Console.Write("Enter an amount of money, with a leading currency symbol (like $): "); string money = Console.ReadLine(); char currSymbol = money[0]; string strAmount = money.Substring(1); // get rest of String double amount = Convert.ToDouble(strAmount); // convert to double ... do stuff with the currency ...
This code is not very robust. If the user pressed Enter without typing a response, it will crash when it tries to access the first character of the user's entry using the [ ] indexer. If the user typed just a currency symbol by itself, the indexer will succeed, but then a crash will occur on the next line when the Substring() method attempts to extract the rest of the string. A robust program must anticipate the possibility of an empty string and guard against it. This could be done with an if statement:
Console.Write("Enter an amount of money, with a leading currency symbol (ex. $1534.25): "); String money = Console.ReadLine(); if (money.Length < 2) { // String is not big enough to contain currency Console.WriteLine("You did not type a response..."); } else { // String is big enough char currSymbol = money[0]; string strAmount = money.Substring(1); // get rest of String double amount = Convert.ToDouble(strAmount); // convert to double ... do stuff with the currency ... }
There are some problems with this "guard against problems with if test" approach. It is difficult and sometimes impossible to anticipate all the possible things that might go wrong. For example, the user might type in a response like "$ABC", which would cause a runtime error when we attempt to convert the "ABC" to a double value. We could write some code to try to check that the user entered only digits (possibly containing a decimal point) after the currency symbol, but that would be fairly tricky to get right. Furthermore, trying to anticipate all the possible problems can turn what would otherwise be a straightforward program into a messy tangle of if statements.
C# provides a neater, more structured alternative approach for dealing with errors that can occur while a program is running. The approach is referred to as exception handling. Let's start by defining what an exception is.
A simple definition of exception is "a runtime error". Examples of exceptions include the errors that occur when you use the Substring() method with an out of bounds index, or when you try to convert a string like "abc" to a double value. But the word "exception" is meant to be more general than "error." It includes any circumstance that arises as the program is executed which is meant to be treated as an exception to the normal flow of control of the program.
When an exception occurs during the execution of a program, we say that the exception is thrown. When this happens, the normal flow of the program is thrown off-track, and the program is in danger of crashing. However, the crash can be avoided if the exception is caught and handled in some way. An exception that is not caught will generally cause the program to crash.
Here's the same code rewritten with an exception handler, shown in bold:
Console.Write("Enter an amount of money, with a leading currency symbol (like $): "); string money = Console.ReadLine(); try { char currSymbol = money[0]; string strAmount = money.Substring(1); // get rest of String double amount = Convert.ToDouble(strAmount); // convert to double ... do stuff with the currency ... } catch { Console.WriteLine("You did not enter a valid currency amount."); }
An exception handler consists of two sections: the try section, and the catch section. If an exception is thrown while the computer is executing code in the try section, code inside the catch section "catches" the exception and prevents a crash. We'll look at the details of how you write exception handlers in the next section, but the thing I want you to notice is that this code is almost as simple as the original, non-robust version. We eliminated the if statement, and didn't have to write any complicated input-checking code to make sure the user entered a valid number.
To catch exceptions in a C# program, you need a try statement. The idea is that you tell the computer to "try" to execute some commands. If it succeeds, all well and good. But if an exception is thrown during the execution of those commands, you can catch the exception and handle it. The basic syntax of the try .. catch statement is as follows:
try { protected-statements } catch { handler-statements }
The computer executes the protected-statements in the "try" block. If no exception occurs during the execution of this block, then the "catch" part of the statement is simply ignored (the handler-statements are not executed). However, if an exception occurs, then the computer jumps immediately to the block containing the handler-statements, without finishing the protected-statements. It executes the handler-statements, and then flow continues with statements after the catch block. By handling the exception in this way, you prevent it from crashing the program.
Have a look at this simple example:
try { Console.Write("Enter a number:"); double num = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("You entered: " + num); } catch { Console.Write("You didn't enter a valid number. "); } Console.WriteLine("Goodbye!");
If the user enters a valid number, the computer displays "You entered: " followed by the number, followed by "Goodbye!". If the user does not enter a valid number, the computer displays "You didn't enter a valid number: ", followed by a short description of the exception that occurred, followed by "Goodbye!".
The statements that you write in the catch section of a try statements should usually notify the user that a problem occurred and give some suggestion as to what to do about it. The trouble that arises when you have more than one statement in the try block is that there's no way for the statements in the catch section to know which statement triggered the exception, so that can limit your ability to give specific feedback to the user about the problem. However, it is possible to write handlers to detect specific exceptions.
Have a look at the following example:
Console.Write("Enter an amount of money, with a leading currency symbol (like $): "); String money = Console.ReadLine(); try { char currSymbol = money[0]; string strAmount = money.Substring(1); // get rest of String double amount = Convert.ToDouble(strAmount); // convert to double ... do stuff with the currency ... } catch (ArgumentOutOfRangeException ex) { Console.WriteLine("You must enter at least a currency symbol: " + ex); } catch (FormatException ex) { Console.WriteLine("You must enter a valid number: " + ex.Message); }
As shown in this example, a try block can be followed by more than one catch block. Each catch block can specify a specific type of exception that it will handle. The syntax works like this:
catch (exception-type exception-parameter} { ... }
The exception-class specifies the type of exception you want to catch, and the exception-parameter is an object that holds information about the exception that occurred. Notice how the Console.WriteLine statement in the example displays the value of the exception parameter's Message property; this usually is a brief, terse description of the problem that occurred.
If the money[0] indexer fails because the money string is too short, an ArgumentOutOfRangeException is thrown, which is handled by the first catch block. If the Convert.ToDouble() method fails, it throws a FormatException, which is handled by the second catch block.
If you are calling a method that throws an exception, you can look up the information for the method in the C# API, and find the name of the exception the method throws. Take a moment to look up the Substring() and ToDouble() methods in the C# API. Look for the Exceptions section in the method information to find the exception(s) the method can throw, and the circumstances under which it throws the exception.
It is usually a good idea to specify which exceptions you want to handle using the technique shown in this example. If you use a generic catch block, you may catch exceptions that you were not expecting to occur (such as null pointer exceptions), and without an exception parameter, your code has no way of displaying an appropriate error message. That may not sound terrible, but it can cause you hours of debugging grief when your program suddenty jumps to the exception handler and you don't know why. If you do want a generic catch block, you should define an exception parameter of type Exception, so you can display the specific error that occurred, like this:
Console.Write("Enter an amount of money, with a leading currency symbol (like $): ");
String money = Console.ReadLine();
try {
char currSymbol = money[0];
string strAmount = money.Substring(1); // get rest of String
double amount = Convert.ToDouble(strAmount); // convert to double
... do stuff with the currency ...
}
catch (Exception ex) {
Console.WriteLine("You must enter a valid currency amount: " + ex);
}
One of the nice things about exceptions is that you don't have to write an exception handler for them in the method where the exception occurs. A method can delegate exception handling to its caller. Here's an example of a program that does this:
using System; class PassTheBuck1 { static int GetNumber(String msg) { Console.Write(msg); int num = Convert.ToInt32(Console.ReadLine()); return num; } static void Main() { Console.Write("Program will pause 15 seconds... "); try { int num1 = GetNumber("Enter the first number:"); int num2 = GetNumber("Enter the second number:"); Console.WriteLine("The sum is: " + (num1 + num2)); } catch (FormatException ex) { Console.WriteLine("You didn't enter a valid number."); } Console.WriteLine("\nBye! "); } }
In the GetNumber() method, if the user enters an invalid number, the ToInt32() method will throw an exception. Since there is no try block in the GetNumber() method, the exception will cause GetNumber to return control to the calling method, Main(). Back in Main(), the exception is handled by the exception handler.
This behavior is called exception propagation. It has the benefit that a single try block in the main method can handle errors for the entire program.