Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Exception Handling & IDisposable

Try-Catch-Finally

Cấu trúc cơ bản

try
{
    // Code có thể throw exception
    var result = PerformOperation();
}
catch (ArgumentException ex)
{
    // Handle specific exception
    Log.Error(ex, "Invalid argument");
    throw; // Re-throw với original stack trace
}
catch (Exception ex)
{
    // Handle general exception
    throw new CustomException("Error occurred", ex); // Wrap exception
}
finally
{
    // Cleanup - luôn chạy dù có exception hay không
    Cleanup();
}

Finally block luôn được thực thi

try
{
    Console.WriteLine("In try block");
    return;
}
finally
{
    Console.WriteLine("In finally block"); // Vẫn được thực thi!
}
// Output:
// In try block
// In finally block

Multiple Catch Blocks

try
{
    // Risky operation
}
catch (FileNotFoundException ex)
{
    // Handle file not found
    Console.WriteLine($"File missing: {ex.FileName}");
}
catch (IOException ex)
{
    // Handle IO errors
    Console.WriteLine($"IO error: {ex.Message}");
}
catch (Exception ex)
{
    // Handle all other exceptions
    Console.WriteLine($"Unexpected error: {ex.Message}");
}

Exception Filters (C# 6+)

try
{
    // Risky operation
}
catch (Exception ex) when (ex is IOException)
{
    // Handle IO exceptions
}
catch (Exception ex) when (ex.Message.Contains("timeout"))
{
    // Handle timeout specifically
}

Anti-patterns cần tránh

// ❌ Bad - Swallow exception
catch (Exception ex)
{
    // Do nothing
}

// ✅ Good - Always log or handle
catch (Exception ex)
{
    Log.Error(ex);
    throw;
}

// ❌ Bad - Lose stack trace
catch (Exception ex)
{
    throw ex; // Resets stack trace
}

// ✅ Good - Preserve stack trace
catch (Exception ex)
{
    throw; // Preserves stack trace
}

Tại sao throw ex reset stack trace?

Khi sử dụng throw ex, CLR (Common Language Runtime) coi đây là một exception mới được ném từ vị trí hiện tại, thay vì tiếp tục truyền exception gốc lên trên. Điều này dẫn đến việc stack trace bị reset và chỉ bắt đầu từ vị trí throw ex.

Cơ chế hoạt động

void MethodA()
{
    MethodB();
}

void MethodB()
{
    MethodC();
}

void MethodC()
{
    throw new InvalidOperationException("Error in C");
}

try
{
    MethodA();
}
catch (Exception ex)
{
    // ❌ throw ex - Stack trace bị reset
    throw ex;
    
    // Stack trace chỉ còn:
    // at Program.Main() in Program.cs:line XX
    // (Mất thông tin MethodA -> MethodB -> MethodC)
}
try
{
    MethodA();
}
catch (Exception ex)
{
    // ✅ throw - Stack trace được giữ nguyên
    throw;
    
    // Stack trace đầy đủ:
    // at Program.MethodC() in Program.cs:line XX
    // at Program.MethodB() in Program.cs:line XX
    // at Program.MethodA() in Program.cs:line XX
    // at Program.Main() in Program.cs:line XX
}

Giải thích chi tiết

Aspectthrow exthrow
Stack traceBị reset từ vị trí throwGiữ nguyên stack trace gốc
Exception objectTạo mới (về mặt stack trace)Giữ nguyên object gốc
DebuggingKhó tìm root causeDễ dàng trace ngược lại
IL Codethrow instructionrethrow instruction

Khi nào dùng throw ex?

Thực tế, hầu như không nên dùng throw ex. Tuy nhiên, có một số trường hợp đặc biệt:

// Trường hợp duy nhất chấp nhận được:
// Khi bạn muốn wrap exception vào một exception khác
catch (Exception ex)
{
    throw new CustomException("Context-specific message", ex);
    // InnerException giữ nguyên stack trace gốc
}

ExceptionDispatchInfo - Giữ stack trace khi re-throw

Nếu bạn cần re-throw exception ở một vị trí khác (ví dụ: trong async scenarios), sử dụng ExceptionDispatchInfo:

using System.Runtime.ExceptionServices;

ExceptionDispatchInfo capturedException = null;

try
{
    // Some operation
}
catch (Exception ex)
{
    capturedException = ExceptionDispatchInfo.Capture(ex);
}

// Later, possibly in a different location
capturedException?.Throw();
// Stack trace vẫn được giữ nguyên!

Rule of thumb: Luôn sử dụng throw; thay vì throw ex; để giữ nguyên stack trace gốc, giúp việc debugging dễ dàng hơn.


IDisposable Interface

Interface Definition

public interface IDisposable
{
    void Dispose();
}

Implementing IDisposable

public class ResourceHolder : IDisposable
{
    private bool _disposed = false;
    private FileStream _fileStream;
    
    public ResourceHolder(string path)
    {
        _fileStream = new FileStream(path, FileMode.Open);
    }
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Dispose managed resources
                _fileStream?.Dispose();
            }
            
            // Free unmanaged resources
            // (if any)
            
            _disposed = true;
        }
    }
    
    ~ResourceHolder()
    {
        Dispose(false);
    }
}

Using Statement

// Traditional using statement
using (var stream = new FileStream("file.txt", FileMode.Open))
{
    // Use stream
} // Dispose() called automatically

// C# 8+ using declaration
using var stream = new FileStream("file.txt", FileMode.Open);
// Use stream
// Dispose() called at end of scope

Tương tác giữa Try-Catch-Finally và IDisposable

Using vs Try-Finally

// Using statement (recommended)
using var resource = new ResourceHolder("file.txt");
resource.DoWork();
// Dispose() called automatically, even if exception occurs

// Equivalent try-finally
ResourceHolder resource = null;
try
{
    resource = new ResourceHolder("file.txt");
    resource.DoWork();
}
finally
{
    resource?.Dispose();
}

Multiple Resources với Using

// Multiple using statements
using var file1 = new FileStream("file1.txt", FileMode.Open);
using var file2 = new FileStream("file2.txt", FileMode.Open);
using var reader = new StreamReader(file1);
using var writer = new StreamWriter(file2);

// All resources disposed in reverse order

Exception trong Dispose

public class SafeResource : IDisposable
{
    private bool _disposed = false;
    
    public void Dispose()
    {
        try
        {
            Dispose(true);
        }
        catch
        {
            // Log but don't throw - Dispose should not throw
            Log.Error("Error during dispose");
        }
        finally
        {
            GC.SuppressFinalize(this);
            _disposed = true;
        }
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Cleanup managed resources
        }
    }
}

Best Practices

  1. Luôn sử dụng using cho IDisposable resources

    // ✅ Good
    using var connection = new SqlConnection(connectionString);
    connection.Open();
    
    // ❌ Bad
    var connection = new SqlConnection(connectionString);
    connection.Open();
    // Forgot to dispose!
    
  2. Không throw exception từ Dispose

    public void Dispose()
    {
        try
        {
            Cleanup();
        }
        catch (Exception ex)
        {
            Log.Error(ex); // Log, don't throw
        }
    }
    
  3. Implement Dispose pattern đúng cách

    public class MyResource : IDisposable
    {
        private bool _disposed = false;
        
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    // Dispose managed resources
                }
                // Free unmanaged resources
                _disposed = true;
            }
        }
    }
    
  4. Kết hợp try-catch với using

    try
    {
        using var resource = new ResourceHolder("file.txt");
        resource.DoWork();
    }
    catch (IOException ex)
    {
        Log.Error(ex, "IO error during operation");
        throw;
    }
    // resource.Dispose() called automatically
    

Khi nào implement IDisposable?

  • Khi class sử dụng unmanaged resources (file handles, database connections, etc.)
  • Khi class chứa các IDisposable members
  • Khi cần deterministic cleanup

Khi nào KHÔNG cần IDisposable?

  • Chỉ sử dụng managed resources
  • Không cần cleanup đặc biệt
  • Object lifetime ngắn và GC có thể handle