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

  1. Be Specific: Catch the most specific exception type possible

  2. Don't Swallow: Only catch exceptions you can actually handle

  3. Use Finally/Using: Always clean up resources properly

  4. Provide Context: Include meaningful information in custom exceptions

  5. Log Appropriately: Log exceptions with sufficient context for debugging

  6. Plan for Failure: Assume things will fail and have recovery strategies

  7. Use Exception Filters: Leverage when clauses for conditional catching

  8. 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!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?