Post

C# - How to create and throw exceptions in C#

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

  • Creating and throwing exceptions from within your code is an important aspect of C# programming.
  • The ability to generate an exception in response to a specific condition, issue, or error helps you to ensure the stability of your application.
  • The exception type that you create depends on the coding issue, and should match the intended purpose of the exception as closely as possible.
  • For example, suppose youā€™re creating a method named GraphData that performs data analysis.
    • The method receives a data array as an input parameter.
    • The method is expecting the input data to be in a particular range.
    • If the method receives data thatā€™s outside the expected range, it creates and throws an exception of type ArgumentException.
    • The exception will be handled somewhere down the call stack by the code thatā€™s responsible for providing the data.
  • Here are some common exception types that you might use when creating an exception:

    • ArgumentException or ArgumentNullException: Use these exception types when a method or constructor is called with an invalid argument value or null reference.
    • InvalidOperationException: Use this exception type when the operating conditions of a method donā€™t support the successful completion of a particular method call.
    • NotSupportedException: Use this exception type when an operation or feature is not supported.
    • IOException: Use this exception type when an input/output operation fails.
    • FormatException: Use this exception type when the format of a string or data is incorrect.
  • The new keyword is used to create an instance of an exception. For example, you can create an instance of the ArgumentException exception type as follows:
1
ArgumentException invalidArgumentException = new ArgumentException();

Configure and throw customized exceptions

  • 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

  • Methods should throw an exception whenever they canā€™t complete their intended purpose. The exception thrown should be based on the most specific exception available that fits the error conditions.

  • Consider a scenario where a developer is working on an application that implements a business process.
  • The business process is dependent on user input.
  • If the input doesnā€™t match the expected data type, the method that implements the business process creates and throws an exception.
  • The exception object can be configured with application specific information in the property values.
  • The following code sample demonstrates the 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
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.
This post is licensed under CC BY 4.0 by the author.