iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Modern Web

現在就學C# 與 ASP.NET Core系列 第 21 篇

Day 21|Exception Handling:程式發生錯誤時怎麼處理?

  • 分享至 

  • xImage
  •  

程式執行時,不一定每件事情都能正常完成。

例如:

檔案不存在
外部服務連線失敗
輸入格式錯誤
Method 收到不合法的參數

當操作無法正常完成時,程式會怎麼執行?

如果目前的 Method 無法處理錯誤,又該由誰處理?

今天要學的就是:

Exception Handling
→ 例外處理

今天的核心:

理解 Exception 如何改變程式執行流程,以及如何在適當的位置處理或傳遞錯誤。


今天會學到

Exception
→ 例外發生後,正常流程如何改變?

try / catch
→ 如何捕捉與處理例外?

throw
→ 如何主動拋出例外?

Exception Propagation
→ 例外如何沿著 Method 呼叫鏈傳遞?

finally
→ 哪些工作不論成功或失敗都需要執行?

實務判斷
→ 什麼時候該 catch、throw,或使用正常流程?

今天最重要的觀念不是:

有錯誤
→ 全部 try-catch

而是:

在真正有能力處理錯誤的位置處理;如果目前無法處理,就讓 Exception 繼續往適當的上層傳遞。


1. Exception 如何改變執行流程?

先看一段程式:

Console.WriteLine("A");

int number =
    int.Parse("abc");

Console.WriteLine("B");

int.Parse() 會嘗試把字串轉成整數。

例如:

"123"
→ 可以轉成 int

但:

"abc"
→ 無法轉成 int

因此:

int.Parse("abc");

會拋出:

FormatException

執行流程:

輸出 A
   ↓
int.Parse("abc")
   ↓
拋出 FormatException
   ↓
目前正常流程中斷
   ↓
尋找可以處理 Exception 的位置

所以:

A

會輸出,但:

B

不會執行。


Exception 是什麼?

可以先理解成:

Exception 是用來表示程式執行期間發生異常狀況的機制;當 Exception 被拋出時,原本的正常執行流程會被中斷。

正常情況:

Method
↓
完成工作
↓
正常結束

發生 Exception:

Method
↓
無法正常完成
↓
拋出 Exception
↓
錯誤處理流程接手

如果目前沒有任何地方處理它,Exception 會繼續往呼叫端傳遞。

如果最後仍然沒有被處理,就會成為:

Unhandled Exception
→ 沒有被處理的例外

並可能造成目前程式流程終止。


2. try / catch:捕捉與處理 Exception

如果某段程式可能拋出 Exception,可以使用:

try
{
}
catch
{
}

例如:

try
{
    Console.WriteLine("A");

    int number =
        int.Parse("abc");

    Console.WriteLine("B");
}
catch (FormatException)
{
    Console.WriteLine(
        "C:輸入格式錯誤"
    );
}

Console.WriteLine("D");

執行結果:

A
C:輸入格式錯誤
D

實際發生什麼事?

步驟 執行內容
1 進入 try,輸出 A
2 int.Parse("abc") 拋出 FormatException
3 跳過 try 中尚未執行的 B
4 找到符合的 catch
5 輸出 C
6 catch 正常完成後,繼續執行 D

流程:

try
↓
輸出 A
↓
int.Parse("abc")
↓
FormatException
↓
跳過 B
↓
catch
↓
輸出 C
↓
離開 try-catch
↓
輸出 D

所以:

try
→ 執行可能拋出 Exception 的程式
catch
→ 發生指定 Exception 時進行處理

要特別注意:

catch 不會讓程式回到發生 Exception 的那一行重新執行。

如果 catch 正常完成,程式會從整個 try-catch 後面繼續。


取得 Exception 的資訊

catch 可以取得這次發生的 Exception:

try
{
    int number =
        int.Parse("abc");
}
catch (FormatException ex)
{
    Console.WriteLine(
        ex.Message
    );
}

這裡的:

ex

就是這次發生的 FormatException。

例如:

ex.Message

可以取得錯誤訊息。

這些資訊在實務上常用於:

Logging
除錯
問題追蹤

而對使用者顯示的訊息,通常會另外整理成比較容易理解的內容。


多個 catch

不同問題可能拋出不同的 Exception。

例如:

try
{
    int number =
        int.Parse(
            "999999999999999999999"
        );
}
catch (FormatException)
{
    Console.WriteLine(
        "輸入格式不正確。"
    );
}
catch (OverflowException)
{
    Console.WriteLine(
        "數字超出 int 的範圍。"
    );
}

整理:

FormatException
→ 格式無法轉換

OverflowException
→ 數值超過可接受範圍

因此:

優先捕捉你知道如何處理的具體 Exception。

如果同時存在具體與一般的 Exception:

catch (FormatException)
{
}
catch (Exception ex)
{
}

順序應該是:

Specific
↓
General

不要因為:

catch (Exception)

能捕捉很多錯誤,就到處使用它。


3. throw:表示 Method 無法正常完成

前面的 Exception 是由:

int.Parse()

拋出的。

我們自己的 Method 也可以主動拋出 Exception。

假設建立訂單時:

Quantity 必須大於 0

可以寫:

void CreateOrder(int quantity)
{
    if (quantity <= 0)
    {
        throw new ArgumentOutOfRangeException(
            nameof(quantity),
            "訂單數量必須大於 0。"
        );
    }

    Console.WriteLine(
        $"建立訂單,數量:{quantity}"
    );
}

呼叫:

CreateOrder(0);

因為:

quantity = 0
↓
quantity <= 0
↓
true

所以會執行:

throw new ArgumentOutOfRangeException(
    nameof(quantity),
    "訂單數量必須大於 0。"
);

接著:

CreateOrder()
↓
正常流程中斷
↓
後面的程式不再執行
↓
Exception 往外傳遞

throw 和顯示訊息不一樣

下面這段:

Console.WriteLine(
    "訂單數量不合法"
);

只是:

顯示訊息
↓
程式仍然可以繼續

但:

throw new ArgumentOutOfRangeException(
    nameof(quantity)
);

則是:

拋出 Exception
↓
目前正常流程中斷
↓
交給 Exception Handling

所以:

throw 不是單純顯示錯誤,而是明確表示目前操作無法正常完成。


return 和 throw

兩者都可能結束目前 Method,但語意不同。

語法 用途
return 正常結束 Method,必要時回傳結果
throw 表示操作無法正常完成,進入 Exception Handling

可以這樣記:

return
→ 正常完成
throw
→ 無法正常完成

4. Exception Propagation:例外會傳到哪裡?

這是今天最重要的觀念之一。

假設程式有三個層次:

呼叫端
   ↓
ProcessOrder()
   ↓
CreateOrder()

程式:

void ProcessOrder()
{
    Console.WriteLine(
        "開始處理訂單"
    );

    CreateOrder(0);

    Console.WriteLine(
        "訂單處理完成"
    );
}

void CreateOrder(int quantity)
{
    if (quantity <= 0)
    {
        throw new ArgumentOutOfRangeException(
            nameof(quantity),
            "訂單數量必須大於 0。"
        );
    }

    Console.WriteLine(
        "訂單建立成功"
    );
}

呼叫端:

try
{
    ProcessOrder();
}
catch (ArgumentOutOfRangeException)
{
    Console.WriteLine(
        "訂單數量不合法。"
    );
}

Console.WriteLine(
    "返回主流程"
);

執行結果:

開始處理訂單
訂單數量不合法。
返回主流程

為什麼沒有:

訂單處理完成

因為:

ProcessOrder()
↓
CreateOrder(0)
↓
throw
↓
CreateOrder 無法正常完成
↓
Exception 回到 ProcessOrder
↓
ProcessOrder 沒有 catch
↓
繼續往呼叫端傳遞
↓
找到 catch

這稱為:

Exception Propagation
→ 例外傳遞

核心:

Exception 會沿著 Method 呼叫鏈往外傳遞,直到找到可以處理它的 catch。


5. Exception 應該在哪一層處理?

學會 try-catch 不代表每一層都應該 catch。

前面的例子可以整理成:

層次 責任
CreateOrder() 知道 quantity 是否符合規則,不合法時 throw
ProcessOrder() 負責協調流程,無法處理就讓 Exception 往外傳
呼叫端 有足夠資訊時進行提示、記錄或其他處理

所以:

不是每個 Method 都需要 try-catch。

判斷的關鍵不是:

這裡可能出錯嗎?
→ 有
→ 加 try-catch

而是:

目前這一層真的知道發生錯誤後應該怎麼處理嗎?

例如目前這一層知道:

要顯示什麼訊息
要不要重試
要怎麼補救
要怎麼回應使用者

那麼:

catch

才有意義。

如果不知道:

不要為了 catch 而 catch。


6. 這個情況真的需要 Exception 嗎?

學會 Exception Handling 後,還有一個很重要的問題:

這個失敗真的屬於 Exception,還是只是正常流程的一部分?


正常、可預期的失敗

例如使用者輸入:

abc

這是很常見、可以預期的情況。

如果使用:

int.Parse(input);

確實可能產生 FormatException。

但這種情況通常更適合:

int.TryParse()

例如:

Console.Write(
    "請輸入數字:"
);

string? input =
    Console.ReadLine();

if (
    int.TryParse(
        input,
        out int number
    )
)
{
    Console.WriteLine(
        $"轉換成功:{number}"
    );
}
else
{
    Console.WriteLine(
        "請輸入有效的整數。"
    );
}

流程:

輸入 "123"
→ true
→ number = 123
輸入 "abc"
→ false
→ 顯示輸入提示

所以:

不要使用 Exception 控制正常、可預期的流程。


如何判斷?

情況 常見方式
正常條件判斷 if
可預期的轉換失敗 TryParse()
違反 Method 的操作前提 throw
目前這層知道如何處理 Exception catch
目前無法合理處理 讓 Exception 往外傳

例如:

使用者輸入可能不是數字
→ TryParse()
Quantity <= 0
違反 CreateOrder() 的操作前提
→ throw
呼叫端知道如何提示使用者
→ catch

7. 常見的 Exception Handling 問題

不要寫空的 catch

這種寫法應該避免:

try
{
    CreateOrder(0);
}
catch (Exception)
{
}

這代表:

Exception 發生
↓
被 catch
↓
什麼都沒做
↓
錯誤被隱藏

這常被稱為:

Swallow Exception
→ 把 Exception 吞掉

問題是:

呼叫端不知道操作失敗
錯誤沒有被記錄
很難除錯
很難追蹤問題

因此:

catch 不是為了讓錯誤消失,而是要做有意義的處理。

例如:

提供提示
記錄 Log
執行補救
轉換錯誤
決定是否重新拋出

補充:記錄後繼續往外傳

有時某一層只能先記錄問題,但真正的處理仍然要交給上層。

例如:

try
{
    CreateOrder(0);
}
catch (ArgumentOutOfRangeException ex)
{
    Console.WriteLine(
        $"記錄錯誤:{ex.Message}"
    );

    throw;
}

這裡:

throw;

代表:

重新拋出目前捕捉到的 Exception,讓它繼續往外傳。

流程:

Exception
↓
catch
↓
記錄資訊
↓
throw;
↓
繼續往上傳

目前先理解它的用途即可。


8. finally:必要的收尾工作

有些程式不論:

正常完成
或
發生 Exception

離開 try 時都需要執行必要的收尾。

這時可以使用:

finally

例如:

try
{
    int number =
        int.Parse("123");

    Console.WriteLine(number);
}
catch (FormatException)
{
    Console.WriteLine(
        "格式錯誤"
    );
}
finally
{
    Console.WriteLine(
        "處理結束"
    );
}

結果:

123
處理結束

如果改成:

int.Parse("abc");

則會得到:

格式錯誤
處理結束

所以在一般的 Exception Handling 流程中:

try 正常完成
↓
finally

或:

try
↓
Exception
↓
catch
↓
finally

可以先理解成:

finally 適合放置不論成功或失敗,都需要執行的收尾工作。


常見用途

例如:

釋放資源
關閉連線
必要的清理工作

不過:

不是每個 try-catch 都需要 finally。

如果沒有必要的清理工作,就不用為了語法完整而刻意加入。

現代 C# 對許多需要釋放的 Resource,也會搭配:

using

處理。

using 與 IDisposable 之後有需要時再深入。


9. 完整 Order 實戰

現在把今天的觀念整合起來。

需求:

建立訂單

Quantity 必須 > 0

CreateOrder()
→ 負責檢查並建立訂單

ProcessOrder()
→ 負責協調流程

呼叫端
→ 負責處理已知錯誤

完整程式:

try
{
    ProcessOrder(
        "A001",
        5
    );

    ProcessOrder(
        "A002",
        0
    );
}
catch (ArgumentOutOfRangeException ex)
{
    Console.WriteLine(
        $"建立失敗:{ex.Message}"
    );
}

Console.WriteLine(
    "返回主流程"
);

void ProcessOrder(
    string orderId,
    int quantity
)
{
    Console.WriteLine(
        $"開始處理訂單 {orderId}"
    );

    CreateOrder(
        orderId,
        quantity
    );

    Console.WriteLine(
        $"訂單 {orderId} 處理完成"
    );
}

void CreateOrder(
    string orderId,
    int quantity
)
{
    if (quantity <= 0)
    {
        throw new ArgumentOutOfRangeException(
            nameof(quantity),
            "訂單數量必須大於 0。"
        );
    }

    Console.WriteLine(
        $"訂單 {orderId} 建立成功,數量:{quantity}"
    );
}

執行結果:

開始處理訂單 A001
訂單 A001 建立成功,數量:5
訂單 A001 處理完成

開始處理訂單 A002
建立失敗:訂單數量必須大於 0。
返回主流程

第二筆訂單發生什麼事?

ProcessOrder("A002", 0)
↓
CreateOrder("A002", 0)
↓
quantity <= 0
↓
throw ArgumentOutOfRangeException
↓
CreateOrder 中斷
↓
Exception 回到 ProcessOrder
↓
ProcessOrder 沒有 catch
↓
繼續往呼叫端傳遞
↓
呼叫端找到 catch
↓
顯示錯誤訊息

因此:

Console.WriteLine(
    $"訂單 {orderId} 處理完成"
);

在第二筆訂單中不會執行。

這個範例最重要的觀念是:

發現問題的位置,不一定等於處理問題的位置。

CreateOrder() 最清楚:

quantity 合不合法

所以由它決定是否:

throw

而呼叫端最清楚:

發生錯誤後應該如何回應

所以由它:

catch

10. Exception Handling 的 Mental Model

今天可以整理成:

Method 執行
     ↓
可以正常完成?
  ↙          ↘
是            否
↓             ↓
正常完成      發生異常
               ↓
             throw
               ↓
        Exception 往外傳
               ↓
      目前這層能處理嗎?
         ↙          ↘
       能            不能
       ↓              ↓
     catch        繼續往上傳

另外:

finally
→ 必要時負責收尾與清理

實務上可以依序問三個問題:

1. 這是正常、可預期的分支嗎?
   → if / TryParse / 一般回傳

2. 是否違反 Method 的操作前提,
   或發生目前無法正常處理的異常?
   → throw

3. 目前這一層真的知道怎麼處理嗎?
   → 知道:catch
   → 不知道:讓 Exception 往外傳

Day 21 小結

今天最重要的概念:

語法 / 概念 用途
try 執行可能拋出 Exception 的程式
catch 在有能力處理時捕捉指定 Exception
throw 表示目前操作無法正常完成
Exception Propagation Exception 沿著 Method 呼叫鏈往外傳遞
finally 執行必要的收尾或清理工作

實務上可以記成:

正常、可預期的流程
→ if / TryParse / 一般回傳
操作無法正常完成
→ throw
目前這一層知道如何處理
→ catch
目前無法處理
→ 讓 Exception 往上傳
需要必要收尾
→ finally

今天最重要的一句話:

Exception Handling 的目的不是把所有錯誤攔下來,而是讓無法正常完成的操作被清楚表示,並在真正有能力處理的位置進行處理。


上一篇
Day 20|Deferred Execution:LINQ 查詢到底什麼時候執行?
下一篇
Day 22|async / await:程式等待工作時,一定要卡住嗎?
系列文
現在就學C# 與 ASP.NET Core 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言