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-catch
pattern consists of atry
block followed by one or morecatch
clauses. Eachcatch
block is used to specify handlers for different exceptions. - The
try-finally
pattern consists of atry
block followed by afinally
block. Typically, the statements of afinally
block run when control leaves atry
statement. - The
try-catch-finally
pattern implements all three types of exception handling blocks. A common scenario for thetry-catch-finally
pattern is when resources are obtained and used in atry
block, exceptional circumstances are managed in acatch
block, and the resources are released or otherwise managed in thefinally
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 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
try
statement. - An exception is thrown in the code block of the inner
try
statement. - The runtime finds the
catch
clause associated with the outertry
statement. - Before the runtime transfers control to the first line of the
catch
code block, it executes thefinally
clause associated with the innertry
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.
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
, andfinally
keywords. Each keyword has an associated code block that serves a specific purpose. - Exceptions are represented as types and derived from the
System.Exception
class 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
catch
clause that can handle it. The search starts with the method where the exception was thrown, and moves down the call stack if necessary.