Exception Handling & Resource Management
Errors and unexpected events (exceptions) are a fact of life in software development. C# provides a robust system to catch, handle, and gracefully recover from these events.
1. The Try-Catch-Finally Pattern
The core mechanism for error handling is the try-catch-finally block.
try
{
// 1. Code that might fail (e.g., file access, network call)
string content = File.ReadAllText("data.txt");
}
catch (FileNotFoundException ex)
{
// 2. Specific handling for missing files
Console.WriteLine($"Error: The file was not found. {ex.Message}");
}
catch (UnauthorizedAccessException ex)
{
// 3. Handling for permission issues
Console.WriteLine("Error: You don't have permission to read this file.");
}
catch (Exception ex)
{
// 4. Catch-all for any other error (use sparingly!)
Console.WriteLine($"A general error occurred: {ex.Message}");
}
finally
{
// 5. Code that ALWAYS runs, regardless of whether an error occurred or not.
// Perfect for "cleaning up" (closing files, releasing database connections).
Console.WriteLine("File operation attempt complete.");
}
The Power of finally
The finally block is guaranteed to run even if there is a return statement in the try or catch blocks. This ensures that critical cleanup logic is never skipped.
2. Most Commonly Used Exceptions
| Exception | When it occurs |
|---|---|
NullReferenceException | Attempting to access a member of a variable that is null. |
ArgumentNullException | A method receives a null argument when it expects a value. |
ArgumentOutOfRangeException | An argument is outside the valid range (e.g., index -1 in a list). |
InvalidOperationException | An object is in a state where an operation cannot be performed. |
IndexOutOfRangeException | Trying to access an array element with an index that doesn't exist. |
KeyNotFoundException | Searching for a key in a Dictionary that isn't there. |
HttpRequestException | Something went wrong with a web request. |
3. Implementing Custom Exceptions
You should create your own exceptions when none of the built-in ones accurately describe the specific business error.
Rules for Custom Exceptions:
- Inherit from
System.Exception(or a more specific base likeInvalidOperationException). - Follow the naming convention: End the class name with
Exception. - Provide at least three constructors (Standard, Message-only, and Message + InnerException).
public class InsufficientFundsException : Exception
{
public decimal AttemptedAmount { get; }
public InsufficientFundsException() : base("Not enough money in the account.") { }
public InsufficientFundsException(string message) : base(message) { }
public InsufficientFundsException(string message, decimal amount) : base(message)
{
AttemptedAmount = amount;
}
public InsufficientFundsException(string message, Exception inner) : base(message, inner) { }
}
// Usage
if (balance < withdrawal)
{
throw new InsufficientFundsException("Withdrawal failed due to low balance.", withdrawal);
}
4. Resource Management: IDisposable
Some objects use "unmanaged" resources (like file handles, network sockets, or database connections). The Garbage Collector (GC) doesn't know how to clean these up immediately.
The IDisposable Interface
Objects that need manual cleanup implement the IDisposable interface, which has a single method: Dispose().
public class DatabaseConnector : IDisposable
{
public void Connect() => Console.WriteLine("Connected to DB");
public void Dispose()
{
// Close connections, release memory, etc.
Console.WriteLine("Connection closed and cleaned up.");
}
}
The using Statement (The Preferred Way)
Instead of calling .Dispose() manually in a finally block, use the using statement. It automatically calls Dispose() for you when the block ends.
The "Old" Way (Classic using block):
using (var connector = new DatabaseConnector())
{
connector.Connect();
// Disposal happens automatically at the closing brace }
}
The "Modern" Way (C# 8.0+ Using Declaration):
void ProcessData()
{
using var connector = new DatabaseConnector();
connector.Connect();
// Disposal happens automatically when the METHOD finishes.
}
5. Best Practices
- Don't swallow exceptions: Never use an empty
catch { }. If you catch it, log it or handle it. - Throw early, catch late: Throw exceptions as soon as a problem is detected, but only catch them where you can actually do something about it.
- Use specific exceptions: Catch
FileNotFoundExceptionbefore catching the genericException.