Skip to content

淺談 throw 與 throw ex 的差異

TLDR

  • catch 區塊中,應優先使用 throw; 而非 throw ex;
  • throw; 能保留原始的 Stack Trace,確保除錯時能定位到錯誤發生的原始行數。
  • throw ex; 會將當前位置視為錯誤起點,導致原始堆疊資訊遺失,應避免使用。
  • 若需封裝錯誤,應建立新的 Exception 並將原始 Exception 作為 innerException 傳入。
  • catch 區塊內僅執行 throw; 而無其他邏輯,應直接移除 try...catch 區塊。
  • .NET 5 以上版本已引入 CA2200 規則,會自動偵測並警告 throw ex 的使用。

throw 和 throw ex 的使用方式

使用 throw 來重新拋出 Exception

什麼情況下會遇到這個問題:當需要在 catch 區塊中攔截錯誤,執行部分邏輯後,仍希望將錯誤繼續向上拋出,且不希望破壞原始錯誤堆疊時。

使用 throw 可以重新拋出捕捉到的 Exception,並保留原始的 Exception 堆疊資訊。這對於除錯非常重要,因為它能夠提供正確的 Exception 發生路徑。

csharp
try {
    try {
        int result = Divide(1, 0);
    } catch {
        throw;
    }
} catch (Exception e) {
    Console.WriteLine(e.ToString());
}

static int Divide(int numerator, int denominator) {
    return numerator / denominator;
}

產生的錯誤訊息如下,可以明確地知道是第 3 行發生錯誤:

text
System.DivideByZeroException: Attempted to divide by zero.
   at Program.<<Main>$>g__Divide|0_0(Int32 numerator, Int32 denominator) in D:\Programming\Projects\TestThrow\TestThrow\Program.cs:line 12
   at Program.<Main>$(String[] args) in D:\Programming\Projects\TestThrow\TestThrow\Program.cs:line 3

使用 throw ex 會導致的問題

什麼情況下會遇到這個問題:開發者誤以為 throw ex 是重新拋出錯誤的標準寫法,卻忽略了它會重置堆疊資訊。

使用 throw ex 則會建立一個新的 Exception 並拋出,這會重置 Exception 的堆疊資訊,使得追蹤 Exception 變得更加困難。

csharp
try {
    try {
        int result = Divide(1, 0);
    } catch (Exception ex) {
        throw ex;
    }
} catch (Exception e) {
    Console.WriteLine(e.ToString());
}

static int Divide(int numerator, int denominator) {
    return numerator / denominator;
}

顯示的錯誤訊息如下,可以發現堆疊資訊裡,錯誤發生變成是第 5 行 throw ex 的位置,導致無法找到真正出錯的行數:

csharp
System.DivideByZeroException: Attempted to divide by zero.
   at Program.<Main>$(String[] args) in D:\Programming\Projects\TestThrow\TestThrow\Program.cs:line 5

TIP

測試顯示使用 throw ex 重新拋出的 Exception,其 InnerExceptionnull,這證實了原本的堆疊資訊確實會遺失。

從 .NET 5 開始,官方增加了 CA2200 的檢核規則,當偵測到 throw ex 的程式碼,會出現警告 CA2200: 重新擲回攔截到的例外狀況變更堆疊資訊。詳細說明請參考 重大變更:CA2200:重新擲回以儲存堆疊詳細資料CA2200: 必須重新擲回以儲存堆疊詳細資料

正確的 Exception 處理方式

什麼情況下會遇到這個問題:當需要對原始 Exception 進行封裝,並添加額外的業務邏輯上下文資訊時。

我們應該重新拋出一個新的 Exception,並將原始 Exception 作為內部 Exception 傳遞。這樣做能夠保留原始 Exception 的資訊,同時增加新的上下文資訊。

csharp
public class CustomException : Exception {
    public CustomException(string message, Exception innerException)
        : base(message, innerException) {
    }
}

try {
    // 可能發生 Exception 的程式碼...
} catch (Exception ex) {
    // ex 要作為 InnerException 傳入,才能保留資訊
    throw new CustomException("額外的資訊", ex);
}

避免不必要的 try...catch

什麼情況下會遇到這個問題:在程式碼重構過程中,遺留了僅包含 throw; 的空 catch 區塊。

如果在 catch 區塊內沒有任何處理僅僅是 throw 的話,實際上可以省略 try...catch,因為這樣的 catch 區塊並沒有任何意義。

csharp
try {
    // 可能發生 Exception 的程式碼...
} catch {
    // 沒做任何處理,只有 throw
    throw;
}

異動歷程

    • 初版文件建立。