Exception Handling
try/catch/finally Basic Mechanism
C# uses three keywords — try, catch, and finally — to build its exception handling mechanism. Code that may throw an exception is placed in the try block, the catch block catches and handles the exception, and the finally block always executes regardless of whether an exception occurred, commonly used for resource cleanup.
try
{
int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
finally
{
Console.WriteLine("finally block always executes");
}
Caught exception: Attempted to divide by zero.
finally block always executes
Multiple catch Blocks
You can catch different types of exceptions in order. More specific exception types should be placed before more general ones.
try
{
int[] arr = { 1, 2, 3 };
Console.WriteLine(arr[10]);
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"Index out of bounds: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"General exception: {ex.Message}");
}
Index out of bounds: Index was outside the bounds of the array.
Common Exception Types
| Exception Type | Trigger Scenario |
|---|---|
FormatException |
Invalid string format, e.g., int.Parse("abc") |
NullReferenceException |
Accessing a member of a null object |
IndexOutOfRangeException |
Array index out of range |
DivideByZeroException |
Integer divided by zero |
OverflowException |
Arithmetic overflow (in checked context) |
FileNotFoundException |
File does not exist |
ArgumentException |
Invalid method argument |
Example
try
{
int num = int.Parse("hello");
}
catch (FormatException)
{
Console.WriteLine("Format exception: string cannot be converted to integer");
}
string s = null;
try
{
Console.WriteLine(s.Length);
}
catch (NullReferenceException)
{
Console.WriteLine("Null reference exception: object is null");
}
Format exception: string cannot be converted to integer
Null reference exception: object is null
Exception Class Hierarchy
The inheritance relationship of all exception classes is as follows:
Object
└─ Exception
└─ SystemException
├─ FormatException
├─ NullReferenceException
├─ IndexOutOfRangeException
├─ DivideByZeroException
├─ OverflowException
├─ FileNotFoundException
└─ ArgumentException
└─ ArgumentNullException
Exception Class Properties
The Exception class provides the following commonly used properties for getting detailed exception information:
| Property | Description |
|---|---|
Message |
Human-readable text describing the exception |
StackTrace |
Call stack information at the time the exception occurred |
InnerException |
The inner exception that caused the current exception |
Source |
Name of the application or object that threw the exception |
TargetSite |
The method that threw the exception |
Example
try
{
int zero = 0;
int result = 100 / zero;
}
catch (Exception ex)
{
Console.WriteLine($"Message: {ex.Message}");
Console.WriteLine($"Source: {ex.Source}");
Console.WriteLine($"TargetSite: {ex.TargetSite}");
}
Message: Attempted to divide by zero.
Source: ConsoleApp
TargetSite: Void Main()
Throwing Exceptions with throw
Use the throw keyword to actively throw an exception. When re-throwing, throw; preserves the original stack trace, while throw ex; resets the stack. Avoid using the latter.
Example
void CheckAge(int age)
{
if (age < 0)
{
throw new ArgumentException("Age cannot be negative", nameof(age));
}
Console.WriteLine($"Age: {age}");
}
try
{
CheckAge(-5);
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
Age cannot be negative (Parameter 'age')
Difference Between throw and throw ex
void InnerMethod()
{
throw new InvalidOperationException("Internal error");
}
void OuterMethod()
{
try
{
InnerMethod();
}
catch (Exception ex)
{
throw;
}
}
try
{
OuterMethod();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
at InnerMethod() in Program.cs:line 2
at OuterMethod() in Program.cs:line 10
throw; to re-throw exceptions. It preserves the complete call stack, making it easier to locate the root cause. throw ex; truncates the stack to the current method, losing the original exception location information.
Exception Filters
C# 6 introduced exception filters with the when clause, allowing conditions to be added to catch. The exception is only caught when the condition evaluates to true.
Example
try
{
throw new HttpRequestException("Network timeout, please retry later");
}
catch (HttpRequestException ex) when (ex.Message.Contains("timeout"))
{
Console.WriteLine("Caught timeout exception, preparing to retry");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Other network exception: {ex.Message}");
}
Caught timeout exception, preparing to retry
Custom Exception Classes
Create custom exception classes by inheriting from Exception. Typically, you need to implement three constructors: a parameterless constructor, a constructor with a message parameter, and a constructor with both message and inner exception parameters.
Example
class InsufficientBalanceException : Exception
{
public decimal Balance { get; }
public decimal Amount { get; }
public InsufficientBalanceException()
: base("Insufficient balance") { }
public InsufficientBalanceException(string message)
: base(message) { }
public InsufficientBalanceException(string message, Exception innerException)
: base(message, innerException) { }
public InsufficientBalanceException(decimal balance, decimal amount)
: base($"Insufficient balance: current balance {balance}, required {amount}")
{
Balance = balance;
Amount = amount;
}
}
class BankAccount
{
public decimal Balance { get; private set; }
public BankAccount(decimal balance)
{
Balance = balance;
}
public void Withdraw(decimal amount)
{
if (amount > Balance)
{
throw new InsufficientBalanceException(Balance, amount);
}
Balance -= amount;
Console.WriteLine($"Withdrawal successful, balance: {Balance}");
}
}
var account = new BankAccount(100);
try
{
account.Withdraw(200);
}
catch (InsufficientBalanceException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine($"Balance: {ex.Balance}, attempted withdrawal: {ex.Amount}");
}
Insufficient balance: current balance 100, required 200
Balance: 100, attempted withdrawal: 200
Exception Handling Best Practices
Catch Specific Exceptions, Not Just Exception
try
{
int value = int.Parse(input);
Console.WriteLine(100 / value);
}
catch (FormatException)
{
Console.WriteLine("Input is not a valid integer");
}
catch (DivideByZeroException)
{
Console.WriteLine("Cannot input zero");
}
catch blocks to swallow exceptions, as this makes problems difficult to diagnose.
Don't Swallow Exceptions
try
{
File.WriteAllText("data.txt", content);
}
catch (Exception)
{
}
The above code hides all errors and is a bad practice. At minimum, you should log the error or re-throw it.
Use finally to Release Resources
FileStream fs = null;
try
{
fs = new FileStream("data.txt", FileMode.Open);
int b = fs.ReadByte();
}
finally
{
fs?.Dispose();
}
Use using Statement Instead of try-finally
Resources that implement the IDisposable interface should prefer the using statement, which automatically calls Dispose at the end of the scope, ensuring resource release even if an exception occurs.
using (var fs = new FileStream("data.txt", FileMode.Open))
{
int b = fs.ReadByte();
}
using statement is equivalent to try-finally with a Dispose() call and is the preferred approach for resource management.
Wrap Exceptions with InnerException
try
{
SaveToFile();
}
catch (IOException ioEx)
{
throw new ApplicationException("Failed to save data", ioEx);
}
By using InnerException, the original exception information is preserved without losing context.
❓ FAQ
throw; and throw ex;?throw; preserves the original stack trace; throw ex; resets the stack to the current method. Use throw; when debugging.finally block not execute?Environment.FailFast) or a StackOverflowException occurs, finally may not execute.try-finally without catch?Exception?Exception (directly or indirectly) to be caught by catch.when?when clause does not catch the exception when the condition doesn't match, allowing the exception to continue propagating. This is more efficient and semantically clearer than checking conditions inside catch.📖 Summary
- The
tryblock wraps code that may error,catchcaptures exceptions,finallyalways executes - Common exception types include
FormatException,NullReferenceException,IndexOutOfRangeException,DivideByZeroException, etc. - The
Exceptionclass providesMessage,StackTrace,InnerException, and other properties throw;preserves the stack;throw ex;resets the stack — use the former- Custom exception classes inherit
Exceptionand implement standard constructors - Exception filter
catch ... when (...)provides conditional capture capability - Don't swallow exceptions, catch specific types, use
usingfor resource management, wrap exceptions withInnerException
📝 Exercises
- Write a method
int SafeParse(string s)that usestry-catchto handleFormatException, returning0and printing a warning when parsing fails - Write a custom exception
InvalidGradeExceptionwith aGradeproperty that is thrown when the grade is not in the 0-100 range - Write code demonstrating the difference between
throw;andthrow ex;, printing bothStackTracevalues and observing the difference - Use exception filter
whento implement: catchFileNotFoundExceptiononly when the file path starts with"config", otherwise let it propagate - Write a file reading method that uses the
usingstatement to ensureStreamReaderis properly released, handling possibleFileNotFoundExceptionandIOException



