C# & ASP.NET Topic
C# Exception Handling
1. Fundamentals of Exception Handling
What are Exceptions?
Exceptions are runtime errors that disrupt the normal flow of program execution. C# uses a structured approach to handle these using try, catch, and finally blocks.
Basic Syntax:
csharp
try
{
// Code that might throw an exception
}
catch (ExceptionType ex)
{
// Code to handle the exception
}
finally
{
// Code that always executes (optional)
}
2. try/catch/finally Blocks
Detailed Examples:
Basic Exception Handling:
csharp
public class BasicExceptionHandling
{
public static void DemonstrateBasicHandling()
{
try
{
Console.Write("Enter a number: ");
string input = Console.ReadLine();
int number = int.Parse(input); // Might throw FormatException
int result = 10 / number; // Might throw DivideByZeroException
Console.WriteLine($"10 divided by {number} is {result}");
}
catch (FormatException ex)
{
Console.WriteLine($"Error: Invalid number format. {ex.Message}");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Error: Cannot divide by zero. {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}
finally
{
Console.WriteLine("This always executes, regardless of exceptions");
}
}
}
// Usage
BasicExceptionHandling.DemonstrateBasicHandling();
Multiple Catch Blocks (Order Matters):
csharp
public class MultipleCatchExample
{
public static void ProcessData()
{
try
{
// Simulate different error scenarios
object[] data = { "hello", 42, null, "123" };
for (int i = 0; i <= data.Length; i++) // Intentional off-by-one
{
string str = (string)data[i]; // Might throw InvalidCastException or NullReferenceException
int length = str.Length; // Might throw NullReferenceException
Console.WriteLine($"Length: {length}");
}
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"Array index error: {ex.Message}");
}
catch (InvalidCastException ex)
{
Console.WriteLine($"Type conversion error: {ex.Message}");
}
catch (NullReferenceException ex)
{
Console.WriteLine($"Null reference error: {ex.Message}");
}
catch (Exception ex) // Catch-all - must be last
{
Console.WriteLine($"General error: {ex.Message}");
}
}
}
Finally Block in Action:
csharp
public class ResourceManagement
{
public static void FileProcessingExample()
{
StreamReader reader = null;
try
{
reader = new StreamReader("nonexistentfile.txt");
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.FileName}");
}
catch (IOException ex)
{
Console.WriteLine($"IO Error: {ex.Message}");
}
finally
{
// Always close the resource, even if exception occurs
reader?.Close();
Console.WriteLine("File stream closed in finally block");
}
}
// Better approach with using statement
public static void BetterFileProcessing()
{
try
{
using (var reader = new StreamReader("data.txt"))
{
string content = reader.ReadToEnd();
Console.WriteLine(content);
} // Automatically calls Dispose() even if exception occurs
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.Message}");
}
}
}
3. Common Exception Types
System Exception Hierarchy:
csharp
public class CommonExceptionTypes
{
public static void DemonstrateCommonExceptions()
{
try
{
// 1. ArgumentException
ValidateAge(-5);
}
catch (ArgumentException ex)
{
Console.WriteLine($"ArgumentException: {ex.Message}");
}
try
{
// 2. InvalidOperationException
Queue<int> queue = new Queue<int>();
int item = queue.Dequeue(); // Throws when queue is empty
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"InvalidOperationException: {ex.Message}");
}
try
{
// 3. FormatException
int number = int.Parse("not-a-number");
}
catch (FormatException ex)
{
Console.WriteLine($"FormatException: {ex.Message}");
}
try
{
// 4. NullReferenceException
string text = null;
int length = text.Length;
}
catch (NullReferenceException ex)
{
Console.WriteLine($"NullReferenceException: {ex.Message}");
}
try
{
// 5. IndexOutOfRangeException
int[] numbers = { 1, 2, 3 };
int value = numbers[5];
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"IndexOutOfRangeException: {ex.Message}");
}
}
private static void ValidateAge(int age)
{
if (age < 0 || age > 150)
throw new ArgumentException("Age must be between 0 and 150", nameof(age));
}
}
4. Custom Exceptions
Creating custom exceptions allows you to express domain-specific error conditions.
Basic Custom Exception:
csharp
// Custom exception base class
public abstract class BankingException : Exception
{
public string AccountNumber { get; }
public DateTime ErrorTime { get; }
protected BankingException(string accountNumber, string message)
: base(message)
{
AccountNumber = accountNumber;
ErrorTime = DateTime.UtcNow;
}
protected BankingException(string accountNumber, string message, Exception innerException)
: base(message, innerException)
{
AccountNumber = accountNumber;
ErrorTime = DateTime.UtcNow;
}
}
// Specific custom exceptions
public class InsufficientFundsException : BankingException
{
public decimal CurrentBalance { get; }
public decimal RequestedAmount { get; }
public InsufficientFundsException(string accountNumber, decimal currentBalance, decimal requestedAmount)
: base(accountNumber, $"Insufficient funds in account {accountNumber}. " +
$"Current: {currentBalance:C}, Requested: {requestedAmount:C}")
{
CurrentBalance = currentBalance;
RequestedAmount = requestedAmount;
}
}
public class AccountNotFoundException : BankingException
{
public AccountNotFoundException(string accountNumber)
: base(accountNumber, $"Account {accountNumber} not found")
{
}
}
public class InvalidTransactionException : BankingException
{
public string TransactionType { get; }
public InvalidTransactionException(string accountNumber, string transactionType, string reason)
: base(accountNumber, $"Invalid {transactionType} transaction for account {accountNumber}: {reason}")
{
TransactionType = transactionType;
}
}
Using Custom Exceptions:
csharp
public class BankAccount
{
private decimal _balance;
public string AccountNumber { get; }
public BankAccount(string accountNumber, decimal initialBalance)
{
AccountNumber = accountNumber;
_balance = initialBalance;
}
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new InvalidTransactionException(AccountNumber, "Withdrawal", "Amount must be positive");
if (amount > _balance)
throw new InsufficientFundsException(AccountNumber, _balance, amount);
_balance -= amount;
Console.WriteLine($"Withdrawn {amount:C}. New balance: {_balance:C}");
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new InvalidTransactionException(AccountNumber, "Deposit", "Amount must be positive");
_balance += amount;
}
public decimal GetBalance() => _balance;
}
public class BankingService
{
private Dictionary<string, BankAccount> _accounts = new Dictionary<string, BankAccount>();
public BankingService()
{
// Initialize with some accounts
_accounts.Add("ACC001", new BankAccount("ACC001", 1000));
_accounts.Add("ACC002", new BankAccount("ACC002", 500));
}
public void ProcessWithdrawal(string accountNumber, decimal amount)
{
try
{
if (!_accounts.ContainsKey(accountNumber))
throw new AccountNotFoundException(accountNumber);
var account = _accounts[accountNumber];
account.Withdraw(amount);
}
catch (BankingException ex)
{
// Log the banking-specific exception
LogBankingError(ex);
throw; // Re-throw to let caller handle it
}
}
private void LogBankingError(BankingException ex)
{
Console.WriteLine($"[{ex.ErrorTime:yyyy-MM-dd HH:mm:ss}] Banking Error:");
Console.WriteLine($" Account: {ex.AccountNumber}");
Console.WriteLine($" Message: {ex.Message}");
Console.WriteLine($" Type: {ex.GetType().Name}");
if (ex is InsufficientFundsException fundsEx)
{
Console.WriteLine($" Current Balance: {fundsEx.CurrentBalance:C}");
Console.WriteLine($" Requested Amount: {fundsEx.RequestedAmount:C}");
}
}
}
5. Exception Handling Best Practices
1. Be Specific in Catch Blocks
csharp
public class BestPractices
{
// DON'T - Too generic
public void ProcessDataBad(string data)
{
try
{
int number = int.Parse(data);
// Process number...
}
catch (Exception ex) // Catches everything
{
Console.WriteLine("Error occurred");
}
}
// DO - Be specific
public bool TryProcessData(string data, out int result)
{
result = 0;
try
{
result = int.Parse(data);
return true;
}
catch (FormatException)
{
Console.WriteLine("Invalid number format");
return false;
}
catch (OverflowException)
{
Console.WriteLine("Number is too large or too small");
return false;
}
}
}
2. Don't Catch What You Can't Handle
csharp
public class ProperExceptionPropagation
{
// DON'T - Swallowing exceptions
public void SaveDataBad(string data)
{
try
{
File.WriteAllText("data.txt", data);
}
catch (Exception ex)
{
// Just logging and continuing - the caller doesn't know it failed!
Console.WriteLine("Failed to save data");
}
}
// DO - Let exceptions propagate when appropriate
public void SaveDataGood(string data)
{
try
{
File.WriteAllText("data.txt", data);
}
catch (UnauthorizedAccessException ex)
{
// We can handle permission issues by trying a different location
string tempPath = Path.GetTempPath();
string tempFile = Path.Combine(tempPath, "data.txt");
File.WriteAllText(tempFile, data);
Console.WriteLine($"Saved to temporary location: {tempFile}");
}
// Let other exceptions (like IOException) propagate to caller
}
// DO - Use exception filters (C# 6.0+)
public void ProcessWithRetry()
{
int retryCount = 0;
while (retryCount < 3)
{
try
{
// Some operation that might fail
ConnectToService();
break; // Success
}
catch (TimeoutException ex) when (retryCount < 2) // Only catch if condition is true
{
retryCount++;
Console.WriteLine($"Timeout occurred, retrying... ({retryCount}/3)");
Thread.Sleep(1000);
}
catch (TimeoutException ex) // Final attempt failed
{
Console.WriteLine("All retry attempts failed");
throw;
}
}
}
private void ConnectToService()
{
// Simulate occasional timeout
if (new Random().Next(0, 2) == 0)
throw new TimeoutException("Service connection timed out");
}
}
3. Proper Resource Management
csharp
public class ResourceManagementBestPractices
{
// DON'T - Manual resource management that might leak
public void ReadFileBad(string path)
{
StreamReader reader = null;
try
{
reader = new StreamReader(path);
// Read file...
}
catch (IOException ex)
{
Console.WriteLine($"IO error: {ex.Message}");
}
finally
{
// What if reader is null? What if Close() throws?
reader?.Close();
}
}
// DO - Use using statements
public string ReadFileGood(string path)
{
try
{
using (var reader = new StreamReader(path))
{
return reader.ReadToEnd();
} // Automatically disposed even if exception occurs
}
catch (FileNotFoundException ex)
{
throw new ApplicationException($"Configuration file not found: {path}", ex);
}
}
// DO - Multiple resources with using
public void CopyFile(string sourcePath, string destPath)
{
try
{
using (var source = new FileStream(sourcePath, FileMode.Open))
using (var destination = new FileStream(destPath, FileMode.Create))
{
source.CopyTo(destination);
}
}
catch (IOException ex)
{
throw new ApplicationException($"Failed to copy {sourcePath} to {destPath}", ex);
}
}
}
4. Exception Logging and Monitoring
csharp
public class ExceptionLogging
{
private readonly ILogger _logger;
public ExceptionLogging(ILogger logger)
{
_logger = logger;
}
public void ProcessOrder(Order order)
{
try
{
ValidateOrder(order);
ChargePayment(order);
ShipOrder(order);
}
catch (PaymentException ex)
{
_logger.LogError(ex, "Payment failed for order {OrderId}", order.Id);
throw new OrderProcessingException("Failed to process payment", ex);
}
catch (ShippingException ex)
{
_logger.LogError(ex, "Shipping failed for order {OrderId}", order.Id);
// Maybe we can recover by notifying customer service
NotifyCustomerService(order, ex);
throw new OrderProcessingException("Failed to ship order", ex);
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Unexpected error processing order {OrderId}", order.Id);
throw;
}
}
private void ValidateOrder(Order order) { /* ... */ }
private void ChargePayment(Order order) { /* ... */ }
private void ShipOrder(Order order) { /* ... */ }
private void NotifyCustomerService(Order order, Exception ex) { /* ... */ }
}
// Mock classes for completeness
public class Order { public string Id { get; set; } }
public class PaymentException : Exception { }
public class ShippingException : Exception { }
public class OrderProcessingException : Exception
{
public OrderProcessingException(string message, Exception inner) : base(message, inner) { }
}
public interface ILogger
{
void LogError(Exception ex, string message, params object[] args);
void LogCritical(Exception ex, string message, params object[] args);
}
5. Global Exception Handling
csharp
public static class GlobalExceptionHandler
{
public static void SetupGlobalHandlers()
{
// Global handler for unhandled exceptions
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
var exception = e.ExceptionObject as Exception;
LogFatalException(exception);
// Log and gracefully shutdown
Console.WriteLine("Fatal application error occurred");
Environment.Exit(1);
};
// Task-based async exceptions
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Console.WriteLine($"Unobserved task exception: {e.Exception}");
e.SetObserved(); // Mark as handled to prevent process crash
};
}
private static void LogFatalException(Exception ex)
{
string logMessage = $"[FATAL] {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} - Unhandled exception:\n" +
$"{ex}";
// In real application, write to file or logging service
File.AppendAllText("fatal_errors.log", logMessage + "\n\n");
Console.WriteLine(logMessage);
}
}
// Usage in application startup
public class Program
{
public static void Main()
{
// Set up global exception handling first thing
GlobalExceptionHandler.SetupGlobalHandlers();
try
{
RunApplication();
}
catch (Exception ex)
{
Console.WriteLine($"Application failed: {ex.Message}");
Environment.Exit(1);
}
}
private static void RunApplication()
{
// Your application logic here
Console.WriteLine("Application running...");
}
}
6. Real-World Complete Example
csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
public class ApiService
{
private readonly HttpClient _httpClient;
public ApiService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetUserDataAsync(int userId)
{
const int maxRetries = 3;
int attempt = 0;
while (attempt < maxRetries)
{
try
{
var response = await _httpClient.GetAsync($"https://api.example.com/users/{userId}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex) when (attempt < maxRetries - 1)
{
attempt++;
Console.WriteLine($"Request failed (attempt {attempt}/{maxRetries}): {ex.Message}");
await Task.Delay(1000 * attempt); // Exponential backoff
}
catch (HttpRequestException ex)
{
throw new ApiServiceException($"Failed to retrieve user data after {maxRetries} attempts",
userId, ex);
}
catch (TaskCanceledException ex)
{
throw new ApiServiceException("Request timeout occurred", userId, ex);
}
}
throw new InvalidOperationException("Unexpected flow reached");
}
}
public class ApiServiceException : Exception
{
public int UserId { get; }
public DateTime ErrorTime { get; }
public ApiServiceException(string message, int userId, Exception innerException = null)
: base(message, innerException)
{
UserId = userId;
ErrorTime = DateTime.UtcNow;
}
public override string ToString()
{
return $"[{ErrorTime:yyyy-MM-dd HH:mm:ss}] ApiServiceException - UserId: {UserId}, Message: {Message}";
}
}
// Usage example
public class Application
{
public static async Task Main()
{
try
{
using (var client = new HttpClient())
{
var apiService = new ApiService(client);
string userData = await apiService.GetUserDataAsync(123);
Console.WriteLine($"User data: {userData}");
}
}
catch (ApiServiceException ex)
{
Console.WriteLine($"API service error: {ex}");
// Handle appropriately - maybe show user-friendly message
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex}");
// Log and potentially restart application
}
}
}
Key Exception Handling Principles Summary
-
Be Specific: Catch the most specific exception type possible
-
Don't Swallow: Only catch exceptions you can actually handle
-
Use Finally/Using: Always clean up resources properly
-
Provide Context: Include meaningful information in custom exceptions
-
Log Appropriately: Log exceptions with sufficient context for debugging
-
Plan for Failure: Assume things will fail and have recovery strategies
-
Use Exception Filters: Leverage
whenclauses for conditional catching -
Global Handlers: Set up global exception handlers as a safety net
Proper exception handling is crucial for building resilient applications that can recover from errors and provide meaningful feedback to users and developers.
Comments (0)
No comments yet. Be the first to comment!