Examine exceptions and the exception handling process
- 11 minutes
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:
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
}
Note
The C# language also enables your code to generate an exception object by using the throw keyword. Exception handling scenarios that include using the throw keyword to generate exceptions is covered in a separate module on Microsoft Learn.
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-catchpattern consists of atryblock followed by one or morecatchclauses. Eachcatchblock is used to specify handlers for different exceptions. - The
try-finallypattern consists of atryblock followed by afinallyblock. Typically, the statements of afinallyblock run when control leaves atrystatement. - The
try-catch-finallypattern implements all three types of exception handling blocks. A common scenario for thetry-catch-finallypattern is when resources are obtained and used in atryblock, exceptional circumstances are managed in acatchblock, and the resources are released or otherwise managed in thefinallyblock.
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 more detailed examination of the Exception class is included later in this module.
A runtime instance of a class is generally referred to as an object, so exceptions are often referred to as exception objects.
Note
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.
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:
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
trystatement. - An exception is thrown in the code block of the inner
trystatement. - The runtime finds the
catchclause associated with the outertrystatement. - Before the runtime transfers control to the first line of the
catchcode block, it executes thefinallyclause associated with the innertrystatement. - The runtime then transfers control to the first line of the
catchcode 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.
Note
For a console application, the entry point to your application is the top-level statements. In the Visual Studio Code call stack, this entry point is referred to as the Main method.
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.
Recap
Here are a few important things to remember from this unit:
- Common scenarios that may require exception handling include user input, data processing, file I/O operations, database operations, and network communication.
- Exception handling in C# is implemented using
try,catch, andfinallykeywords. Each keyword has an associated code block that serves a specific purpose. - Exceptions are represented as types and derived from the
System.Exceptionclass in .NET. Exceptions contain information that identifies the type of exception, and properties that provide additional details. - When an exception occurs, the .NET runtime searches for the nearest
catchclause that can handle it. The search starts with the method where the exception was thrown, and moves down the call stack if necessary.