C# - Exception Handling
C# - Modern Exception Handling
Section titled “C# - Modern Exception Handling”What Is an Exception?
Section titled “What Is an Exception?”An exception is an error condition or unexpected event that occurs during the execution of a program. In C#, an exception is an object that is created and “thrown” when a library or application code encounters an exceptional circumstance, such as attempting to divide by zero or accessing a file that doesn’t exist.
Exceptions provide a structured, robust, and type-safe way to handle errors, separating error-handling code from the main program logic.
Core Keywords for Exception Handling
Section titled “Core Keywords for Exception Handling”C# provides a powerful mechanism for handling exceptions built upon four primary keywords:
- try: The
tryblock encloses the code that might throw an exception. It’s the “protected” region of your code. - catch: The
catchblock is an exception handler. It executes when a specific type of exception is thrown within the correspondingtryblock. You can have multiplecatchblocks to handle different exception types. - finally: The
finallyblock contains code that always executes, regardless of whether an exception was thrown or caught. It’s ideal for releasing resources like file handles or database connections. - throw: The
throwkeyword is used to signal the occurrence of an exception. You can either throw a new exception or re-throw an existing one from within acatchblock.
Exception Handling Structure
Section titled “Exception Handling Structure”The fundamental structure for handling exceptions involves a try block followed by one or more catch blocks, and optionally, a finally block.
try{ // Code that might cause an exception. // For example, file operations or network requests.}catch (SpecificExceptionType ex1){ // Code to handle the specific exception. // Log the error, notify the user, etc.}catch (AnotherExceptionType ex2) when (ex2.SomeCondition){ // Using an exception filter (when clause) to conditionally catch. // This is useful for more fine-grained error handling.}catch (Exception ex){ // A general catch block for any other exceptions. // It's best practice to catch specific exceptions first.}finally{ // Code that will always execute. // Perfect for cleanup tasks like closing a file.}Modern C# also provides the using declaration, which simplifies resource management and often replaces the need for an explicit finally block to dispose of resources.
The Exception Hierarchy in .NET
Section titled “The Exception Hierarchy in .NET”In .NET, all exceptions derive from the base class System.Exception. This class provides common properties like Message (a description of the error) and StackTrace (information about where the error occurred).
There are two main branches of built-in exceptions derived from System.Exception:
- System.SystemException: The base class for exceptions thrown by the .NET runtime itself (CLR). Examples include
NullReferenceExceptionandIndexOutOfRangeException. - System.ApplicationException: This was historically intended as a base class for user-defined exceptions. However, modern best practice is to derive custom exceptions directly from
System.Exceptioninstead.
Here are some common built-in exception classes:
| Exception Class | Description |
|---|---|
System.IO.IOException | Base class for I/O-related errors, like FileNotFoundException. |
System.IndexOutOfRangeException | Thrown when attempting to access an array or collection with an invalid index. |
System.NullReferenceException | Thrown when attempting to access a member of a null object. |
System.DivideByZeroException | Thrown during integer division by zero. |
System.InvalidCastException | Thrown when an explicit type conversion fails. |
System.OutOfMemoryException | Thrown when the application runs out of available memory. |
System.ArgumentException | Base class for exceptions related to invalid method arguments, like ArgumentNullException. |
Practical Examples
Section titled “Practical Examples”Example 1: Basic try-catch-finally
Section titled “Example 1: Basic try-catch-finally”This example demonstrates handling a DivideByZeroException. The finally block executes even though an exception is caught.
// This example can be run using top-level statements in modern C#.
int SafeDivide(int numerator, int denominator){ int result = 0; try { Console.WriteLine("Attempting to divide..."); result = numerator / denominator; Console.WriteLine("Division successful."); } catch (DivideByZeroException ex) { // Using string interpolation for cleaner output. Console.WriteLine($"Error: Cannot divide by zero. Details: {ex.Message}"); } finally { Console.WriteLine("The 'finally' block is always executed."); Console.WriteLine($"Final result variable is: {result}"); } return result;}
SafeDivide(25, 5); // Successful caseConsole.WriteLine("---_---");SafeDivide(25, 0); // Exception case
/** Expected Output:** Attempting to divide...* Division successful.* The 'finally' block is always executed.* Final result variable is: 5* ---_---* Attempting to divide...* Error: Cannot divide by zero. Details: Attempted to divide by zero.* The 'finally' block is always executed.* Final result variable is: 0*/Creating Custom Exceptions
Section titled “Creating Custom Exceptions”Creating your own exception types is a great way to represent specific error conditions in your application’s domain. Best practice is to derive directly from System.Exception and follow a standard naming convention by ending the class name with Exception.
Example 2: A Custom InvalidTemperatureException
Section titled “Example 2: A Custom InvalidTemperatureException”// A custom exception for a specific domain error.public class InvalidTemperatureException : Exception{ public InvalidTemperatureException(string message) : base(message) { }}
public class Thermostat{ public void SetTemperature(int tempInCelsius) { // Absolute zero is -273.15°C. if (tempInCelsius < -273) { throw new InvalidTemperatureException($"{tempInCelsius}°C is below absolute zero!"); } Console.WriteLine($"Temperature set to {tempInCelsius}°C"); }}
// Main execution logicvar thermostat = new Thermostat();try{ thermostat.SetTemperature(25); thermostat.SetTemperature(-300);}catch (InvalidTemperatureException ex){ Console.WriteLine($"Caught a custom domain error: {ex.Message}");}
/** Expected Output:** Temperature set to 25°C* Caught a custom domain error: -300°C is below absolute zero!*/Re-throwing Exceptions
Section titled “Re-throwing Exceptions”Sometimes you need to catch an exception, perform an action (like logging), and then let the exception continue up the call stack. It is critical to use throw; for this, not throw ex;.
throw;: Re-throws the original exception, preserving its original stack trace. This is the correct way to re-throw.throw ex;: Throws a new exception, resetting the stack trace to the current location. This hides the original source of the error and should be avoided unless you are intentionally wrapping the exception.
void ProcessData(){ try { // Simulate a low-level operation throw new InvalidOperationException("Data corruption detected."); } catch (InvalidOperationException ex) { Console.WriteLine("Logging error at low level..."); // Correctly re-throw to allow higher-level handlers to act. throw; }}
try{ ProcessData();}catch (Exception ex){ Console.WriteLine("Caught at top level. See full stack trace below:"); Console.WriteLine(ex.StackTrace);}Best Practices and Common Mistakes
Section titled “Best Practices and Common Mistakes”Best Practices
Section titled “Best Practices”- Catch Specific Exceptions: Avoid catching the generic
System.Exceptionunless you are at the top level of your application (e.g., in a global error handler). Catching specific exceptions makes your code more predictable and robust. - Use
usingforIDisposable: For objects that implementIDisposable(likeFileStreamorHttpClient), prefer ausingstatement or declaration to ensure resources are always released. The compiler turns this into atry/finallyblock for you. - Don’t Use Exceptions for Control Flow: Exceptions are for exceptional circumstances, not for normal program logic. They are slow and can make code hard to follow. Use
ifstatements or methods likeint.TryParsefor expected conditions. - Include a
InnerException: When you catch an exception and wrap it in a new, more specific one, pass the original exception into the new one’s constructor. This preserves the full error chain.
Common Mistakes
Section titled “Common Mistakes”- Swallowing Exceptions: A
catchblock that does nothing (catch (Exception) {}) is dangerous. It hides problems and makes debugging nearly impossible. - Catching and Re-throwing with
throw ex: As mentioned, this destroys the original stack trace. Always usethrow;. - Overusing Custom Exceptions: Only create custom exceptions when they represent a unique, actionable error in your application’s domain. Don’t create a new exception type for every possible error.