The best way to avoid user errors is to not give the user the ability to make them in the first place. For example, suppose a program can take purchase orders for between 1 and 100 reams of paper. If the program lets you specify the quantity by using a NumericUpDown
control with Minimum
= 1 and Maximum
= 100, you cannot accidentally enter invalid values like –5 or 10,000.
Sometimes, however, it's hard to build an interface that protects against all possible errors. For example, if the user needs to type in a numeric value, you need to worry about invalid inputs such as 1.2.3 and ten. If you write a program that works with files, you can't always be sure the file will be available when you need it. For example, it might be on a CD that has been removed, or it might be locked by another program.
In this lesson, you learn how to deal with these kinds of unexpected errors. You learn how to protect against invalid values, unavailable files, and other problems that are difficult or impossible to predict and prevent.
An error is a mistake. It occurs when the program does something incorrect. Sometimes an error is a bug, for example, if the code just doesn't do the right thing.
Sometimes an error is caused by circumstances outside of the program's control. If the program expects the user to enter a numeric value in a textbox but the user types “seven,” the program won't be able to continue its work until the user fixes the problem.
Sometimes you can predict when an error may occur. For example, if a program needs to open a file, there's a chance that the file won't exist. In predictable cases such as this one, the program should try to anticipate the error and protect itself. It should check to see if the file exists before it tries to open it. It can then display a message to the user and ask for help.
Other errors are hard or impossible to predict. Even if the file exists, it may be locked by another program. The user entering invalid data is another example. In those cases, the program may need to just try to do its job. If the program tries to do something seriously invalid, it will receive an exception.
An exception tells the program that something generally very bad occurred such as trying to divide by zero, trying to access an entry in an array that doesn't exist (for example, setting values[100] = 100
when values
only holds 10 items), or trying to convert the text “pickle” into an integer.
In cases like these, the program must catch the exception and deal with it. Sometimes it can figure out what went wrong and fix the problem. Other times it might only be able to tell the user about the problem and hope the user can fix it.
To catch an exception, a program uses a try-catch
block.
In C#, you can use a try
-catch
block to catch exceptions. One common form of this statement has the following syntax:
try
{
...codeToProtect...
}
catch (ExceptionType1 ex
)
{
...exceptionCode1…
}
catch (ExceptionType2 ex
)
{
...exceptionCode2…
}
finally
{
...finallyCode…
}
Where:
codeToProtect
is the code that might throw the exception.ExceptionType1, ExceptionType2
are exception types such as FormatException
or DivideByZeroException
. If this particular exception type occurs in the codeToProtect
, the corresponding catch
block executes.ex
is a variable that has the same type as the exception. You pick the name for this variable just as you do when you declare any other variable. If an error occurs, you can use this variable to learn more about what happened.exceptionCode
is the code that the program should execute if the corresponding exception occurs.finallyCode
is code that always executes whether or not an error occurs.A try
-catch
block can include any number of catch
blocks with different exception types. If an error occurs, the program looks through the catch
blocks in order until it finds one that matches the error. It then executes that block's code and jumps to the finally
statement if there is one.
If you use a catch
statement without an exception type and variable, that block catches all exceptions.
A try
-catch
block must include at least one catch
block or the finally
block, although none of them needs to contain any code. For example, the following code catches and ignores all exceptions:
try
{
...codeToProtect...
}
catch
{
}
The code in the finally
block executes whether or not an exception occurs. If an error occurs, the program executes a catch
block (if one matches the exception) and then executes the finally
block. If no error occurs, the program executes the finally
block after it finishes the codeToProtect
code.
In fact, if the code inside the try
or catch
section executes a return
statement, the finally
block still executes before the program actually leaves the method! The finally
block executes no matter how the code leaves the try-catch
block.
One place where problems are likely to occur is when a program parses text entered by the user. Even if users don't enter obviously ridiculous values such a “twelve,” they might enter values in a format that you don't expect. For example, you might expect the user to enter an integer dollar amount such as 1200 but the user enters $1,200.00. If you use the decimal
data type's Parse
method and don't allow the currency symbol, thousands separator, and decimal point, the Parse
method will throw an exception.
You can use a try-catch
block to handle the exception, but it's more efficient to detect the invalid format instead. To do that, you can use the decimal
data type's TryParse
method.
A data type's TryParse
method attempts to parse some text and save the result in a parameter passed with the out
keyword. The TryParse
method returns true
if it successfully parsed the text and false
if it could not.
For example, the following code tries to parse a value entered by the user:
decimal amount;
if (!decimal.TryParse(amountTextBox.Text, out amount))
{
MessageBox.Show("Invalid format for amount: " +
amountTextBox.Text +
"
The amount should be an integer such as 12.");
return;
}
The code uses decimal.TryParse
to try to parse the value in amountTextBox
. If TryParse
returns false
, the code displays an error message and then uses a return
statement to stop processing the value.
The TryParse
methods can take a NumberStyles
parameter just as the Parse
methods can. For example, you can pass decimal.TryParse
the parameter NumberStyles.Any
to allow the user to enter values that include currency symbols and thousands separators.
To make things a bit more confusing, the version of TryParse
that takes a NumberStyles
parameter also takes a format provider that gives the method information about the culture it should use when parsing the text. If you set that parameter to null
, the method uses the program's current culture information. For example, the following code is similar to the previous code except it allows thousands separators. The new code is highlighted in bold:
decimal amount;
if (!decimal.TryParse(amountTextBox.Text,
NumberStyles.AllowThousands, null,
out amount))
{
MessageBox.Show("Invalid format for amount: " +
amountTextBox.Text +
"
The amount should be an integer such as 12.");
return;
}
It's generally considered good programming practice to look for the most predictable errors first and only use try-catch
blocks as a last resort. That usually allows you to give the user the most meaningful error messages.
Occasionally it's useful to be able to throw your own exceptions. For example, consider the factorial method you wrote in Lesson 20 and suppose the program invokes the method passing it the value –10 for its parameter. The value –10! is not defined, so what should the method do? It could just declare that –10! is 1 and return that, but that approach could hide a bug in the rest of the program.
A better solution is to throw an exception telling the program what's wrong. The calling code can then use a try
-catch
block to catch the error and tell the user what's wrong.
The following code shows an improved version of the factorial method described in Lesson 20. Before calculating the factorial, the code checks its parameter and, if the parameter is less than zero, it throws a new ArgumentOutOfRangeException
. The exception's constructor has several overloaded versions. The one used here takes as parameters the name of the parameter that caused the problem and a description of the error:
// Return value!
private long Factorial(long value)
{
// Check the parameter.
if (value < 0)
{
// This is invalid. Throw an exception.
throw new ArgumentOutOfRangeException(
"value",
"value must be at least 0.");
}
// Calculate the factorial.
long result = 1;
for (long i = 2; i <= value; i++)
{
result *= i;
}
return result;
}
The following code shows how the program might invoke the new version of the Factorial
method. It uses a try
-catch
block to protect itself in case the Factorial
method throws an exception. The block also protects against other errors such as the user entering garbage in the TextBox
.
// Calculate the factorial.
private void calculateButton_Click(object sender, EventArgs e)
{
try
{
// Get the input value.
long number = long.Parse(numberTextBox.Text);
// Calculate the factorial.
long answer = Factorial(number);
// Display the factorial.
resultTextBox.Text = answer.ToString();
}
catch (Exception ex)
{
// Display an error message.
MessageBox.Show(ex.Message);
resultTextBox.Clear();
}
}
In this Try It, you add validation and error handling code to the program you built for Exercise 19-4. When the user clicks the NewItemForm
's Calculate and OK buttons, the program should verify that the values make sense and protect itself against garbage such as the user entering the quantity “one,” as shown in Figure 21.1.
In this lesson, you:
ValuesAreOk
method to validate the values entered by the user. It should:
TryParse
methods to get the Price Each and Quantity values.decimal
value.ValuesAreOk
finds a problem, it should:
false
.ValuesAreOk
finds that all of the values are okay, it should return true
.DialogResult
property doesn't automatically close the form.ValuesAreOk
method to validate the values entered by the user. It should:
TryParse
methods to get the Price Each and Quantity values.decimal
value.ValuesAreOk
finds a problem, it should:
false
.// Try to parse PriceEach.
if (!decimal.TryParse(priceEachTextBox.Text,
NumberStyles.Any, null, out PriceEach))
{
MessageBox.Show("Price Each must be a currency value.");
priceEachTextBox.Focus();
return false;
}
When you parse quantity, you could use NumberStyles.Integer
to require a plain integer, or you could use NumberStyles.AllowThousands
to allow thousands separators.
PriceEach
is greater than zero:// Verify that PriceEach is greater than zero.
if (PriceEach <= 0)
{
MessageBox.Show("Price each must be greater than 0.");
priceEachTextBox.Focus();
return false;
}
decimal
data type:
// See if Quantity * PriceEach is too big.
try
{
decimal total = Quantity * PriceEach;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
You can test that part of the code by setting Price Each to 1e28 and Quantity to 1000.
ValuesAreOk
finds that all of the values are okay, it should return true
.
ItemName = itemTextBox.Text
to save the item name for the main program to read.return true
.decimal
, the main program will crash. You can test this by entering two items with Price Each 1e28 and Quantity 7.
Use a try-catch
block to protect the main program from this problem. Enclose the code that displays the NewItemForm
in a loop that executes as long as the new item's values cause problems.
(Did you anticipate this problem? How about the problem of a new item having a price of $1e28 and quantity 1000? Anticipating and protecting against these kinds of problems is part of what makes programming challenging.)
Copy the program you wrote for Exercise 1 and add sanity checks. Modify the ValuesAreOk
method so it allows up to 100 items and Price Each up to $100.
try-catch
block in the Calculate button's Click
event handler to protect against format errors.To test the program, run it, type some text, and then close the program. Then:
Test.rtf
in the program's executable directory. Then try to use SimpleEdit to open the file.Test.rtf
.In all three tests, Word should have the Test.rtf
file locked so SimpleEdit should display an error message.
Build a program similar to the one shown in Figure 21.2 that calculates solutions to quadratic equations. Hints:
TryParse
to protect against format errors.Math.Sqrt
to take square roots.if
statements to avoid trying to take the square root of a negative number.NaN
(which stands for “not a number”). After performing the calculation, use double.IsNaN
to see if the result is NaN
and display “Not a quadratic” if it is.Button
only when a TextBox
contains non-blank text. If the user should enter a number, you can improve the program by only enabling the Button
if the text has a valid format. Try this out by writing a program that calculates the area of a circle. Hints:
TryParse
to make the TextBox
's TextChanged
event handler enable the Calculate Button
when the user has entered a valid double
and that value is at least zero.TextBox
for each of the basic data types byte
, sbyte
, ushort
, short
, uint
, int
, ulong
, long
, float
, double
, decimal
, bool
, and char
. Use event handlers to set each TextBox
's background color to white if the TextBox
contains a valid value of the corresponding data type and pink if it does not.