Introduction
Exceptions can be thrown by your code when an issue or error condition is encountered. Exception objects that describe an error are created and then thrown with the throw keyword. When an exception is thrown by your code, the runtime searches for the nearest catch clause that can handle the exception.
Suppose youāre working on a data processing application for a company. The application relies on business rules and specifications to ensure that purchase order processing and inventory management tasks are completed appropriately. In addition, the application must use specific language to inform the user when data anomalies and other issues are encountered. Customized exceptions must be thrown, caught, and reflected in the applicationās user interface when issues are encountered. Business rules provide specific guidance in the following categories:
- Data input requirements for the processes.
- Success and failure criteria for the processes.
- Sequence order requirements for the processes.
- Process failure reporting and mitigation requirements.
Create an exception object
1
| ArgumentException invalidArgumentException = new ArgumentException();
|
The process for throwing an exception object involves creating an instance of an exception-derived class, optionally configuring properties of the exception, and then throwing the object by using the throw keyword.
Itās often helpful to customize an exception with contextual information before itās thrown. You can provide application specific information within an exception object by configuring its properties. For example, the following code creates an exception object named invalidArgumentException with a custom Message property, and then throws the exception:
1
2
| ArgumentException invalidArgumentException = new ArgumentException("ArgumentException: The 'GraphData' method received data outside the expected range.");
throw invalidArgumentException;
|
- When customizing an exception object, itās important to provide clear error messages that describe the problem and how to resolve it.
- You can also include additional information such as stack traces and error codes to help users correct the issue.
- An exception object can also be created directly within a throw statement.
- For example:
1
| throw new FormatException("FormatException: Calculations in process XYZ have been cancelled due to invalid data format.");
|
Some considerations to keep in mind when throwing an exception include:
- The Message property should explain the reason for the exception. However, information thatās sensitive, or that represents a security concern shouldnāt be put in the message text.
- The StackTrace property is often used to track the origin of the exception. This string property contains the name of the methods on the current call stack, together with the file name and line number in each method thatās associated with the exception. A StackTrace object is created automatically by the common language runtime (CLR) from the point of the throw statement. Exceptions must be thrown from the point where the stack trace should begin.
When to throw an exception
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| string[][] userEnteredValues = new string[][]
{
new string[] { "1", "two", "3"},
new string[] { "0", "1", "2"}
};
foreach (string[] userEntries in userEnteredValues)
{
try
{
BusinessProcess1(userEntries);
}
catch (Exception ex)
{
if (ex.StackTrace.Contains("BusinessProcess1") && (ex is FormatException))
{
Console.WriteLine(ex.Message);
}
}
}
static void BusinessProcess1(string[] userEntries)
{
int valueEntered;
foreach (string userValue in userEntries)
{
try
{
valueEntered = int.Parse(userValue);
// completes required calculations based on userValue
// ...
}
catch (FormatException)
{
FormatException invalidFormatException = new FormatException("FormatException: User input values in 'BusinessProcess1' must be valid integers");
throw invalidFormatException;
}
}
}
|
- In this code sample,
- the top-level statements call the BusinessProcess1 method, passing in a string array that contains user entered values.
- The BusinessProcess1 method expects user input values that can be converted to an integer.
- When the method encounters data with an invalid format, it creates an instance of the FormatException exception type using a customized Message property.
- The method then throws the exception.
- The exception is caught in the top-level statements as an object named ex.
- Properties of the ex object are examined before displaying the exception message to the user.
- First, the code examines the StackTrace property to see if it contains āBusinessProcess1ā.
- Second, the exception object ex is verified to be of type FormatException.
Re-throwing exceptions
- In addition to throwing a new exception, throw can be used re-throw an exception from inside a catch code block.
- In this case, throw does not take an exception operand.
1
2
3
4
5
6
7
8
| catch (Exception ex)
{
// handle or partially handle the exception
// ...
// re-throw the original exception object for further handling down the call stack
throw;
}
|
- When you re-throw an exception, the original exception object is used, so you donāt lose any information about the exception.
- If you want to create a new exception object that wraps the original exception, you can pass the original exception as an argument to the constructor of a new exception object.
- For example:
1
2
3
4
5
6
7
8
| catch (Exception ex)
{
// handle or partially handle the exception
// ...
// create a new exception object that wraps the original exception
throw new ApplicationException("An error occurred", ex);
}
|
- For the āBusinessProcess1ā application scenario, consider the following updates:
- The BusinessProcess1 method has been updated to include additional details.
- BusinessProcess1 now encounters two issues and must generate exceptions for each issue.
- The top-level statements have been updated.
- Top-level statements now call the OperatingProcedure1 method.
- OperatingProcedure1 calls BusinessProcess1 within a try code block.
- The OperatingProcedure1 method is able to handle one of the exception types and partially handle the other.
- Once the partially handled exception is processed, OperatingProcedure1 must re-throw the original exception.
- The following code sample demonstrates the updated scenario:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
| try
{
OperatingProcedure1();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine("Exiting application.");
}
static void OperatingProcedure1()
{
string[][] userEnteredValues = new string[][]
{
new string[] { "1", "two", "3"},
new string[] { "0", "1", "2"}
};
foreach(string[] userEntries in userEnteredValues)
{
try
{
BusinessProcess1(userEntries);
}
catch (Exception ex)
{
if (ex.StackTrace.Contains("BusinessProcess1"))
{
if (ex is FormatException)
{
Console.WriteLine(ex.Message);
Console.WriteLine("Corrective action taken in OperatingProcedure1");
}
else if (ex is DivideByZeroException)
{
Console.WriteLine(ex.Message);
Console.WriteLine("Partial correction in OperatingProcedure1 - further action required");
// re-throw the original exception
throw;
}
else
{
// create a new exception object that wraps the original exception
throw new ApplicationException("An error occurred - ", ex);
}
}
}
}
}
static void BusinessProcess1(string[] userEntries)
{
int valueEntered;
foreach (string userValue in userEntries)
{
try
{
valueEntered = int.Parse(userValue);
checked
{
int calculatedValue = 4 / valueEntered;
}
}
catch (FormatException)
{
FormatException invalidFormatException = new FormatException("FormatException: User input values in 'BusinessProcess1' must be valid integers");
throw invalidFormatException;
}
catch (DivideByZeroException)
{
DivideByZeroException unexpectedDivideByZeroException = new DivideByZeroException("DivideByZeroException: Calculation in 'BusinessProcess1' encountered an unexpected divide by zero");
throw unexpectedDivideByZeroException;
}
}
}
|
- The updated sample code produces the following output:
1
2
3
4
5
6
| FormatException: User input values in 'BusinessProcess1' must be valid integers
Corrective action taken in OperatingProcedure1
DivideByZeroException: Calculation in 'BusinessProcess1' encountered an unexpected divide by zero
Partial correction in OperatingProcedure1 - further action required
DivideByZeroException: Calculation in 'BusinessProcess1' encountered an unexpected divide by zero
Exiting application.
|
Things to avoid when throwing exceptions
- The following list identifies practices to avoid when throwing exceptions:
- Donāt use exceptions to change the flow of a program as part of ordinary execution. Use exceptions to report and handle error conditions.
- Exceptions shouldnāt be returned as a return value or parameter instead of being thrown.
- Donāt throw System.Exception, System.SystemException, System.NullReferenceException, or System.IndexOutOfRangeException intentionally from your own source code.
- Donāt create exceptions that can be thrown in debug mode but not release mode. To identify runtime errors during the development phase, use Debug.Assert instead.
- The
Debug.Assert
method is a tool for catching logic errors during development. - By default, the Debug.Assert method works only in debug builds.
- You can use Debug.Assert in debug sessions to check for a condition that should never occur.
- The method takes two parameters:
- a Boolean condition to check,
- and an optional string message to display if the condition is false.
- Debug.Assert should not be used in place of throwing an exception, which is a way to handle exceptional situations during normal execution of your code.
- You should use Debug.Assert to catch errors that should never occur,
- and use exceptions to handle errors that could occur during normal execution of your program.