C# - Exception handling
- Runtime errors in a C# application are managed using a mechanism called exceptions.
- Exceptions provide a structured, uniform, and type-safe way of handling both system level and application-level error conditions.
- Exceptions are generated by the .NET runtime or by the code in an application.
Common scenarios that require exception handling
There are several programming scenarios that require exception handling. Many of these scenarios involve some form of data acquisition. Although some of the scenarios involve coding techniques that are outside the scope of this training, theyâre still worth noting.
- Common scenarios that require exception handling include:
- User input: Exceptions can occur when code processes user input. For example, exceptions occur when the input value is in the wrong format or out of range.
- Data processing and computations: Exceptions can occur when code performs data calculations or conversions. For example, exceptions occur when code attempts to divide by zero, cast to an unsupported type, or assign a value thatâs out of range.
- File input/output operations: Exceptions can occur when code reads from or writes to a file. For example, exceptions occur when the file doesnât exist, the program doesnât have permission to access the file, or the file is in use by another process.
- Database operations: Exceptions can occur when code interacts with a database. For example, exceptions occur when the database connection is lost, a syntax error occurs in a SQL statement, or a constraint violation occurs.
- Network communication: Exceptions can occur when code communicates over a network. For example, exceptions occur when the network connection is lost, a timeout occurs, or the remote server returns an error.
- Other external resources: Exceptions can occur when code communicates with other external resources. Web Services, REST APIs, or third-party libraries, can throw exceptions for various reasons. For example, exceptions occur due to network connections issues, malformed data, etc.
Exception handling keywords, code blocks, and patterns
- Exception handling in C# is implemented by using the try, catch, and finally keywords. Each of these keywords has an associated code block and can be used to satisfy a specific goal in your approach to exception handling. For example:
1
2
3
4
5
6
7
8
9
10
11
12
try
{
// try code block - code that may generate an exception
}
catch
{
// catch code block - code to handle an exception
}
finally
{
// finally code block - code to clean up resources
}
The
try
code block contains the guarded code that may cause an exception. If the code within a try block causes an exception, the exception is handled by a corresponding catch block.The
catch
code block contains the code thatâs executed when an exception is caught. The catch block can handle the exception, log it, or ignore it. A catch block can be configured to execute when any exception type occurs, or only when a specific type of exception occurs.The
finally
code block contains code that executes whether an exception occurs or not. The finally block is often used to clean up any resources that are allocated in a try block. For example, ensuring that a variable has the correct or required value assigned to it.Exception handling in a C# application is generally implemented using one or more of the following patterns:
- The try-catch pattern consists of a try block followed by one or more catch clauses. Each catch block is used to specify handlers for different exceptions.
- The try-finally pattern consists of a try block followed by a finally block. Typically, the statements of a finally block run when control leaves a try statement.
- The try-catch-finally pattern implements all three types of exception handling blocks. A common scenario for the try-catch-finally pattern is when resources are obtained and used in a try block, exceptional circumstances are managed in a catch block, and the resources are released or otherwise managed in the finally block.
How are exceptions represented in code?
- Exceptions are represented in code as objects, which means theyâre an instance of a class.
- The .NET class library provides exception classes thatâre accessed in code just like other .NET classes.
Another example of .NET class thatâs used as an object in code is the Random class (used to create random numbers).
- More precisely, exceptions are types, represented by classes that are all ultimately derived from System.Exception.
An exception class thatâs derived from Exception includes information that identifies the type of exception and contains properties that provide details about the exception.
- A runtime instance of a class is generally referred to as an object, so exceptions are often referred to as exception objects.
- Although they are sometimes used interchangeably, a class and an object are different things.
- A class defines a type of object, but itâs not an object itself.
- An object is a concrete entity based on a class.
Generally speaking, your code will catch one of the following:
- An exception object thatâs an instance of the System.Exception base class.
- An exception object thatâs an instance of an exception type that inherits from the base class. For example, an instance of the InvalidCastException class.
Here are the properties of the Exception class:
- Data: The Data property holds arbitrary data in key-value pairs.
- HelpLink: The HelpLink property can be used to hold a URL (or URN) to a help file that provides extensive information about the cause of an exception.
- HResult: The HResult property can be used to access to a coded numerical value thatâs assigned to a specific exception.
- InnerException: The InnerException property can be used to create and preserve a series of exceptions during exception handling.
- Message: The Message property provides details about the cause of an exception.
- Source: The Source property can be used to access the name of the application or the object that causes the error.
- StackTrace: The StackTrace property contains a stack trace that can be used to determine where an error occurred.
- TargetSite: The TargetSite property can be used to get the method that throws the current exception.
Exception handling process
- When an exception occurs, the .NET runtime searches for the nearest catch clause that can handle the exception.
The process begins with the method that caused the exception to be thrown.
- First, the method is examined to see whether the code that caused the exception is inside a try code block.
- If the code is inside try code block, the catch clauses associated with the try statement are considered in order.
- If the catch clauses are unable to handle the exception, the method that called the current method is searched.
- This method is examined to determine whether the method call (to the first method) is inside a try code block.
- If the call is inside a try code block, the associated catch clauses are considered.
- This search process continues until a catch clause is found that can handle the current exception.
- Once a catch clause is found that can handle the exception, the runtime prepares to transfer control to the first statement of the catch block.
However, before execution of the catch block begins, the runtime executes any finally blocks associated with try statements found during the search.
- If more than one finally block is found, they are executed in order, starting with the one closest to the code that caused the exception to be thrown.
If no catch clause is found to handle the exception, the runtime terminates the application and displays an error message to the user.
- Consider the following code sample that includes a try-finally pattern nested inside a try-catch pattern:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try
{
// Step 1: code execution begins
try
{
// Step 2: an exception occurs here
}
finally
{
// Step 4: the system executes the finally code block associated with the try statement where the exception occurred
}
}
catch // Step 3: the system finds a catch clause that can handle the exception
{
// Step 5: the system transfers control to the first line of the catch code block
}
- In this example, the following process occurs:
- Execution begins in the code block of the outer try statement.
- An exception is thrown in the code block of the inner try statement.
- The runtime finds the catch clause associated with the outer try statement.
- Before the runtime transfers control to the first line of the catch code block, it executes the finally clause associated with the inner try statement.
- The runtime then transfers control to the first line of the catch code block and executes the code that handles the exception.
- In this simple example, the nested try-catch and try-finally patterns reside within a single method, but multiple try-catch and try-finally patterns could be spread between methods that call other methods.
Exception handling and the call stack
Youâll often see the term âcall stack unwindingâ when you read about exception handling and the exception handling process. To understand this term, you need to understand the call stack and how itâs used to track the âstackâ of method calls during code execution.
You can think of the call stack like a tower of blocks. When you build a tower, you start with just one block. Each time you add a block to the tower, you place it on top of the existing blocks. When your application starts running in the debugger, the entry point to your application is the first layer added to the call stack (the first block of the tower). Each time a method calls another method, the new method is added to the top of the stack. When your code exits out of a method, the method is removed from the call stack.
Call stack unwinding is the process that the .NET runtime uses when a C# program encounters an error. Itâs the same process that you just reviewed.
Returning to the block tower analogy, when you need to remove a block from the tower, you start from the top and remove each block until you reach the one you need. This process is similar to how call stack unwinding works, where each call layer in the stack is like a block in the tower. When the runtime needs to unwind the call stack, it starts from the top and removes each call layer until it reaches the one that has what it needs. In this case, the call layer that it needs is the method that has a catch clause that can handle the exception that occurred.
Compiler-generated exceptions
The .NET runtime throws exceptions when basic operations fail. Hereâs a short list of runtime exceptions and their error conditions:
- ArrayTypeMismatchException: Thrown when an array canât store a given element because the actual type of the element is incompatible with the actual type of the array.
- DivideByZeroException: Thrown when an attempt is made to divide an integral value by zero.
- FormatException: Thrown when the format of an argument is invalid.
- IndexOutOfRangeException: Thrown when an attempt is made to index an array when the index is less than zero or outside the bounds of the array.
- InvalidCastException: Thrown when an explicit conversion from a base type to an interface or to a derived type fails at runtime.
- NullReferenceException: Thrown when an attempt is made to reference an object whose value is null.
- OverflowException: Thrown when an arithmetic operation in a checked context overflows.
Access the properties of an exception object
- Consider the following code snippet that demonstrates how to access the properties of an exception object:
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
try
{
Process1();
}
catch
{
Console.WriteLine("An exception has occurred");
}
Console.WriteLine("Exit program");
static void Process1()
{
try
{
WriteMessage();
}
catch
{
Console.WriteLine("Exception caught in Process1");
}
}
static void WriteMessage()
{
double float1 = 3000.0;
double float2 = 0.0;
int number1 = 3000;
int number2 = 0;
Console.WriteLine(float1 / float2);
Console.WriteLine(number1 / number2);
}
- Although the catch clause can be used without arguments, that approach is not recommended.
- If you donât specify an argument, all exception types are caught and you canât decern between them.
- In general, you should only catch the exceptions that your code knows how to recover from.
- Therefore, your catch clause should specify an object argument thatâs derived from System.Exception.
- The exception type should be as specific as possible.
This helps to avoid catching exceptions that your exception handler isnât able to resolve.
- Update the Process1 method as follows:
1
2
3
4
5
6
7
8
9
10
11
static void Process1()
{
try
{
WriteMessage();
}
catch (Exception ex)
{
Console.WriteLine($"Exception caught in Process1: {ex.Message}");
}
}
- Running the debugger on the updated code will generate the following output:
1
2
3
â
Exception caught in Process1: Attempted to divide by zero.
Exit program
- The exceptionâs Message property is included in the output generated by your application
Catch a specific exception type
- Now that you know the type of exception to catch, you can update your catch clause to handle that specific exception type.
- Update the Process1 method as follows:
1
2
3
4
5
6
7
8
9
10
11
static void Process1()
{
try
{
WriteMessage();
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Exception caught in Process1: {ex.Message}");
}
}
- Although the reported messages are the same, there is an important difference. Your Process1 method will only catch exceptions of the specific type that itâs prepared to handle.
- To generate a different exception type, update the WriteMessage method as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void WriteMessage()
{
double float1 = 3000.0;
double float2 = 0.0;
int number1 = 3000;
int number2 = 0;
byte smallNumber;
Console.WriteLine(float1 / float2);
// Console.WriteLine(number1 / number2);
checked
{
smallNumber = (byte)number1;
}
}
- Notice the use of the checked statement:
- When performing integral type calculations that assign the value of one integral type to another integral type, the result depends on the overflow-checking context. In a checked context, the conversion succeeds if the source value is within the range of the destination type. Otherwise, an OverflowException is thrown. In an unchecked context, the conversion always succeeds, and proceeds as follows:
- If the source type is larger than the destination type, then the source value is truncated by discarding its âextraâ most significant bits. The result is then treated as a value of the destination type.
- If the source type is smaller than the destination type, then the source value is either sign-extended or zero-extended so that itâs of the same size as the destination type. Sign-extension is used if the source type is signed; zero-extension is used if the source type is unsigned. The result is then treated as a value of the destination type.
- If the source type is the same size as the destination type, then the source value is treated as a value of the destination type.
- Integral type calculations that are not inside a checked code block are treated as if they are inside an unchecked code block.
- Notice that a new exception type is caught by the catch clause in the top-level statements rather than inside the Process1 method.
- Your application prints the following messages to the console:
1
2
3
4
â
An exception has occurred
Exit program
- The catch block in Process1 is not executed.
- This is the behavior that you wanted.
- Only catch the exceptions that your code is prepared to handle.
Catch multiple exceptions in a code block
- At this point, you may be wondering what happens when multiple exceptions occur in a single code block.
- Will your code catch each exception as they occur?
- Update the WriteMessage method as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void WriteMessage()
{
double float1 = 3000.0;
double float2 = 0.0;
int number1 = 3000;
int number2 = 0;
byte smallNumber;
Console.WriteLine(float1 / float2);
Console.WriteLine(number1 / number2);
checked
{
smallNumber = (byte)number1;
}
}
- Set breakpoint inside the WriteMessage() method on the following code line:
1
Console.WriteLine(float1 / float2);
- start a debug session.
- Step through your code one line at a time, and notice what happens after your code handles the first exception.
- When the first exception occurs, control is passed to the first catch clause that can handle the exception.
- The code that would generate the second exception is never reached.
- This means that some of your code is never executed.
- This could lead to serious issues.
Under what conditions would it be undesirable to catch subsequent exceptions?
- Consider the case when your method (or code block) is completing a two part process.
- Assume that the second part of the process is dependent on the first part completing.
- If the first part of the process is unable to complete successfully, thereâs no point in continuing on to the second part of the process.
- In this case, itâs often better to present the user with a message explaining the error condition without attempting the remaining portion or portions of the larger process.
Catch separate exception types in a code block
- There are times when variations in your data may cause different types of exceptions.
- consider the following code snippet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// inputValues is used to store numeric values entered by a user
string[] inputValues = new string[]{"three", "9999999999", "0", "2" };
foreach (string inputValue in inputValues)
{
int numValue = 0;
try
{
numValue = int.Parse(inputValue);
}
catch (FormatException)
{
Console.WriteLine("Invalid readResult. Please enter a valid number.");
}
catch (OverflowException)
{
Console.WriteLine("The number you entered is too large or too small.");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
- First, the code creates a string array named inputValues.
- The data in the array is intended to represent the input values entered by a user who was instructed to enter numeric values.
- Depending on the value entered, different exception types may occur.
- Notice that the code uses the
int.Parse
method to convert the string âinputâ values to integers. - The
int.Parse
code is placed inside a try code block.
- Set a breakpoint on the following code line:
1
numValue = int.Parse(inputValue);
- Save your code, and then start a debug session.
- Step through the code one line at a time, and notice that different exception types are caught.