本文 AI 產出,尚未審核

Rust 錯誤處理 – panic!


簡介

在 Rust 中,錯誤處理分為 可恢復錯誤Result)與 不可恢復錯誤panic!)。
panic! 代表程式在執行時遇到了無法安全繼續的情況,會立即終止當前執行緒,並向上層傳遞一個錯誤訊息。雖然在大多數情況下我們希望透過 Result 來處理錯誤,但在以下情境中,panic! 仍是最直接、最符合語意的選擇:

  • 程式的前提條件(precondition)被破壞,例如傳入了不合法的參數。
  • 內部不變式(invariant)被違反,表示程式本身已經進入了未定義狀態。
  • 測試或開發階段需要快速定位錯誤來源。

了解 panic! 的運作機制、如何自訂錯誤訊息以及在何時該使用它,對於寫出 安全且可維護 的 Rust 程式至關重要。


核心概念

1. panic! 的基本語法

panic! 是一個 ,接受類似 format! 的參數,會在執行時拋出一個 panic。最簡單的寫法:

panic!("發生致命錯誤");

如果想要插入變數:

let value = 42;
panic!("不允許的值:{}", value);

2. Panic 與 unwind / abort

編譯時可以選擇兩種 panic 行為:

行為 說明
unwind(預設) 產生堆疊回溯(stack unwind),允許 catch_unwind 捕獲 panic,適合測試或需要清理資源的情況。
abort 直接終止程式,不產生回溯,執行檔大小較小,適合嵌入式或對效能極度敏感的應用。

Cargo.toml 中設定:

[profile.release]
panic = "abort"

3. 捕獲 panic:std::panic::catch_unwind

雖然大多數情況下不建議捕獲 panic,但在 測試框架插件系統多執行緒容錯 時會用到:

use std::panic;

let result = panic::catch_unwind(|| {
    // 可能會 panic 的程式碼
    panic!("故意觸發");
});

match result {
    Ok(_) => println!("沒有 panic"),
    Err(err) => println!("捕獲到 panic: {:?}", err),
}

⚠️ catch_unwind 只能捕獲 unwind 模式的 panic;若編譯為 abort,此函式會直接導致程式終止。

4. 自訂 panic 鉤子:set_hook

Rust 允許在程式啟動時註冊一個全域的 panic 鉤子,用來改寫錯誤訊息的輸出方式(例如寫入日誌檔):

use std::panic;

fn main() {
    panic::set_hook(Box::new(|info| {
        // 只顯示簡短訊息,避免泄漏敏感資訊
        eprintln!("程式發生致命錯誤: {}", info);
    }));

    // 觸發 panic,會走自訂的 hook
    panic!("測試自訂 hook");
}

5. Panic 與多執行緒

在多執行緒環境中,每個執行緒的 panic 只會終止該執行緒,不會直接影響其他執行緒或主執行緒(除非使用 join 時傳回錯誤):

use std::thread;

let handle = thread::spawn(|| {
    panic!("子執行緒失敗");
});

match handle.join() {
    Ok(_) => println!("子執行緒正常結束"),
    Err(err) => println!("子執行緒 panic: {:?}", err),
}

程式碼範例

以下提供 5 個實用範例,展示 panic! 在不同情境下的使用方式。

範例 1:檢查函式前提條件

fn divide(dividend: i32, divisor: i32) -> i32 {
    // 若除數為 0,直接 panic,因為此情況無法安全回傳結果
    if divisor == 0 {
        panic!("除數不能為 0");
    }
    dividend / divisor
}

fn main() {
    println!("{}", divide(10, 2)); // 5
    // println!("{}", divide(10, 0)); // 會 panic
}

重點:使用 panic! 來表達「此函式在此情況下不應該被呼叫」。

範例 2:使用 catch_unwind 捕獲 panic(測試用)

use std::panic;

fn may_panic(flag: bool) {
    if flag {
        panic!("故意觸發 panic");
    }
}

fn main() {
    let ok = panic::catch_unwind(|| may_panic(false));
    assert!(ok.is_ok());

    let err = panic::catch_unwind(|| may_panic(true));
    assert!(err.is_err());
}

範例 3:自訂 panic 鉤子寫入日誌

use std::panic;
use std::fs::OpenOptions;
use std::io::Write;

fn init_panic_logger() {
    panic::set_hook(Box::new(|info| {
        let mut file = OpenOptions::new()
            .create(true)
            .append(true)
            .open("panic.log")
            .unwrap();

        let _ = writeln!(file, "Panic 發生: {}", info);
    }));
}

fn main() {
    init_panic_logger();
    panic!("這是一個測試 panic,會寫入 panic.log");
}

範例 4:在多執行緒中處理 panic

use std::thread;

fn main() {
    let handles: Vec<_> = (0..3).map(|i| {
        thread::spawn(move || {
            if i == 1 {
                panic!("執行緒 {} 發生 panic", i);
            }
            println!("執行緒 {} 正常結束", i);
        })
    }).collect();

    for h in handles {
        match h.join() {
            Ok(_) => println!("子執行緒完成"),
            Err(e) => eprintln!("捕獲到子執行緒 panic: {:?}", e),
        }
    }
}

範例 5:在 abort 模式下減少二進位大小

# Cargo.toml
[profile.release]
panic = "abort"
fn main() {
    // 在 release 版會直接 abort,沒有 unwind 的成本
    panic!("此程式在 release 時會直接 abort");
}

常見陷阱與最佳實踐

陷阱 說明 建議的做法
把所有錯誤都寫成 panic! 會讓程式在可恢復的錯誤情況下直接崩潰,使用者體驗差。 只在 不可能恢復程式邏輯錯誤 時使用 panic!,其他情況使用 ResultOption
在庫(crate)中直接 panic! 使用者無法自行決定錯誤處理方式,可能破壞上層應用。 提供 Result API,或在 panic! 前加上 #[must_use] 的警告。
忘記在 Cargo.toml 設定 panic = "abort" 在嵌入式或性能敏感的環境仍使用 unwind,導致不必要的程式碼膨脹。 依需求明確設定 panic 行為,並在 CI 中測試兩種模式。
catch_unwind 中使用 ? ? 只能傳播 Result,無法捕獲 panic!,會導致編譯錯誤。 若需要同時處理 Resultpanic!,分別使用 matchResult::map_err 包裝。
set_hook 中執行可能 panic 的程式 若 hook 本身 panic,會導致 雙重 panic,最終 abort。 Hook 內部保持簡單、避免使用可能失敗的 I/O,或使用 std::panic::resume_unwind 重新拋出。

最佳實踐

  1. 明確文件化:在函式或模組的文件註解中說明何時會 panic。
  2. 使用 debug_assert!:在開發階段檢查不變式,發佈版會自動移除,避免不必要的 panic。
    debug_assert!(index < vec.len(), "索引超出範圍");
    
  3. 最小化 panic 範圍:將可能 panic 的程式碼封裝在獨立函式,方便測試與未來改寫。
  4. 在測試中使用 #[should_panic]:驗證程式在不合法輸入時確實會 panic。
    #[test]
    #[should_panic(expected = "除數不能為 0")]
    fn test_divide_by_zero() {
        divide(10, 0);
    }
    

實際應用場景

場景 為什麼選擇 panic!
CLI 工具的參數驗證 若使用者提供了錯誤的參數,直接 panic 可以快速終止並顯示錯誤訊息,避免後續不必要的運算。
嵌入式系統的致命錯誤 設備在硬體資源錯誤(例如記憶體損毀)時,最安全的做法是立即 abort,防止不確定行為。
測試框架 Rust 的測試框架本身就是以 panic 為基礎,#[should_panic] 能驗證程式在特定條件下會崩潰。
插件/腳本執行環境 主程式需要保護自己不被惡意或錯誤的插件拖垮,使用 catch_unwind 包住插件執行,插件內部若 panic,主程式仍能繼續運行。
資料庫交易的不可恢復錯誤 若交易過程中發現資料不一致,最安全的方式是 abort 當前交易(panic),讓外層回滾機制接管。

總結

panic! 在 Rust 中扮演 不可恢復錯誤 的角色,提供了一條清晰且語意明確的路徑,讓程式在遇到「此時此刻已無法安全繼續」的情況下立刻停止。掌握以下要點,才能在實務開發中正確運用:

  • 僅在真正不可恢復的情況下使用,其餘錯誤應以 Result/Option 處理。
  • 了解 unwind vs abort 的差異,根據目標平台與效能需求選擇編譯設定。
  • 透過 catch_unwindset_hook 以及 多執行緒的 join,在需要的時候安全捕獲或記錄 panic。
  • 文件化測試 是避免意外 panic 的最佳防線。

熟練 panic! 的使用方式,能讓你的 Rust 程式在面對致命錯誤時保持可預測、易除錯的特性,同時不犧牲程式的安全與效能。祝你在 Rust 的錯誤處理旅程中,寫出更穩定、更可靠的程式碼!