Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> C#

วิธี Re-throw InnerException โดยไม่สูญเสีย Stack Trace ใน C#

ใน C# การ Re-throw Exception อย่างถูกต้องสำคัญมากสำหรับการ Debugging เพราะ Stack Trace บอกที่มาของ Error

วิธีที่ผิด (❌ Loses Stack Trace)

try
{
    DoWork();
}
catch (Exception ex)
{
    // ❌ สร้าง Exception ใหม่ - Stack Trace ชี้ที่นี่!
    throw new Exception("Error occurred", ex); 
    
    // หรือ
    throw ex; // ❌ Reset Stack Trace ให้ชี้ที่นี่ (Bad!)
}

วิธีที่ถูกต้อง (✅ Preserves Stack Trace)

1. ใช้ throw; เพียงอย่างเดียว (Best Practice)

static void Main()
{
    try
    {
        Method2();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Caught in Main:\n{ex.StackTrace}");
    }
}

static void Method2()
{
    try
    {
        Method1();
    }
    catch
    {
        // ✅ Re-throw Original Exception - Stack Trace คงเดิม
        throw; 
    }
}

static void Method1()
{
    throw new InvalidOperationException("Original Error");
}

ผลลัพธ์ Stack Trace (ถูกต้อง)

Caught in Main:
   at Program.Method1() in Program.cs:line 30
   at Program.Method2() in Program.cs:line 22
   at Program.Main() in Program.cs:line 10

กรณีต้อง Wrap Exception (Add Context)

หากต้องเพิ่ม Context ให้ใช้ ExceptionDispatchInfo (.NET 4.5+) หรือ Constructor ที่รับ innerException

1. ExceptionDispatchInfo.Capture (Best for Preserving Exact Stack Trace)

using System.Runtime.ExceptionServices;

try
{
    RiskyOperation();
}
catch (Exception ex)
{
    // Capture Current Stack Trace
    ExceptionDispatchInfo edi = ExceptionDispatchInfo.Capture(ex);
    
    // ทำ Logging/Context อื่นๆ
    Logger.Log("Failed in RiskyOperation", ex);
    
    // Re-throw พร้อม Stack Trace เดิมสมบูรณ์
    edi.Throw(); // หรือ throw edi.SourceException; (แต่ Throw() ดีกว่า)
}

2. ใช้ Constructor (innerException) - Standard Way

try
{
    ProcessOrder(orderId);
}
catch (Exception ex)
{
    // ✅ เก็บ Original Stack Trace ใน InnerException
    throw new OrderProcessingException($"Failed to process order {orderId}", ex);
}

// Custom Exception
public class OrderProcessingException : Exception
{
    public OrderProcessingException(string message, Exception inner) 
        : base(message, inner) { }
}

3. Throw Helper Method (Pattern)

public static void ThrowWithContext(Exception ex, string context)
{
    // Log context
    Logger.Error($"{context}: {ex.Message}");
    
    // Re-throw preserving stack trace
    ExceptionDispatchInfo.Capture(ex).Throw();
}

// ใช้งาน
catch (Exception ex)
{
    ThrowWithContext(ex, "UserService.GetUser");
}

Async Exception Handling

// Async Method - Exception ถูก Wrap ใน Task
public async Task GetUserAsync(int id)
{
    try
    {
        return await _repo.GetAsync(id);
    }
    catch (Exception ex)
    {
        // ✅ throw; ใน async ก็ Preserve Stack Trace ได้ (Compiler Handle)
        throw; 
    }
}

// Caller
try
{
    var user = await GetUserAsync(1);
}
catch (Exception ex)
{
    // Stack Trace จะชี้ไปที่ _repo.GetAsync และ GetUserAsync
}

สรุป Best Practices

Scenario Code Stack Trace
Re-throw เดิม throw; ✅ คงเดิมสมบูรณ์
Wrap + Context throw new MyEx("msg", ex); ✅ ใน InnerException
Re-throw หลายระดับ throw; ทุกระดับ ✅ คงเดิม
Log แล้ว Re-throw Log(ex); throw; ✅ คงเดิม
❌ Throw ex throw ex; ❌ Reset ให้ชี้ Catch Block
❌ New Exception throw new Ex("msg"); ❌ หายไป (No Inner)

เครื่องมือช่วย

  • Ben.Demystifier (NuGet): Enhanced Stack Trace อ่านง่ายขึ้น
  • Serilog/NLog: Log Exception.Full (รวม InnerExceptions)