本文 AI 產出,尚未審核

Rust 錯誤傳播與處理

簡介

在任何程式語言中,錯誤處理都是不可或缺的環節。Rust 以「安全」為設計核心,提供了兩套互補的錯誤型別:Result<T, E>(可恢復錯誤)與 panic!(不可恢復錯誤)。對於大多數應用程式,我們希望錯誤能夠向上層傳播,讓呼叫者決定如何回應,而不是在最底層直接終止程式。

本單元將說明:

  • 為什麼要使用 Result 進行錯誤傳播
  • Rust 提供的便利語法(?try!map_err 等)
  • 實務上常見的錯誤類型與自訂錯誤
  • 如何在真實專案中把錯誤處理寫得既簡潔可維護

核心概念

1. Result<T, E> 的基本用法

Result 是一個列舉(enum),有兩個變體:

enum Result<T, E> {
    Ok(T),      // 成功,攜帶值 T
    Err(E),    // 失敗,攜帶錯誤 E
}
  • 成功 時回傳 Ok(value),呼叫端可以直接使用 value
  • 失敗 時回傳 Err(error),錯誤資訊會被傳遞至上層。

範例 1:讀取檔案內容

use std::fs::File;
use std::io::{self, Read};

fn read_file(path: &str) -> Result<String, io::Error> {
    // 開檔可能失敗 → 回傳 Err
    let mut file = File::open(path)?;
    let mut contents = String::new();
    // 讀取可能失敗 → 回傳 Err
    file.read_to_string(&mut contents)?;
    Ok(contents)   // 成功 → 回傳 Ok
}
  • ? 會自動把 Err 轉交給呼叫者,等同於 match 的樣板程式碼。

2. ? 運算子:錯誤向上層傳播的捷徑

? 只能在回傳型別為 Result<_, E>(或 Option<_>)的函式內使用。它的行為如下:

表達式 結果
Ok(v) 取出 v,繼續執行後續程式碼
Err(e) 立即返回 Err(e),不執行後面的程式碼

範例 2:多層函式呼叫

fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse::<i32>()      // 直接返回 Result
}

fn double_number(s: &str) -> Result<i32, std::num::ParseIntError> {
    let n = parse_number(s)?;   // 若 parse 失敗,錯誤直接傳遞出去
    Ok(n * 2)
}

3. 自訂錯誤型別與 thiserror

在較大的專案中,我們常會把不同來源的錯誤統一成一個自訂列舉,方便 match 處理。thiserror crate 能自動為列舉產生 DisplayError 實作。

範例 3:自訂錯誤

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("IO 錯誤: {0}")]
    Io(#[from] std::io::Error),

    #[error("解析錯誤: {0}")]
    ParseInt(#[from] std::num::ParseIntError),

    #[error("設定檔遺失: {0}")]
    ConfigMissing(String),
}

使用方式:

fn load_config(path: &str) -> Result<String, AppError> {
    let mut cfg = std::fs::read_to_string(path)?; // 自動轉換為 AppError::Io
    if cfg.is_empty() {
        return Err(AppError::ConfigMissing(path.to_string()));
    }
    Ok(cfg)
}

4. map_errand_then 與其他組合子

有時候我們想在錯誤傳遞的同時改變錯誤類型加入額外資訊Result 的方法能讓程式碼保持流暢。

範例 4:把底層錯誤包裝成自訂錯誤

fn parse_config(s: &str) -> Result<serde_json::Value, AppError> {
    serde_json::from_str(s).map_err(|e| AppError::ParseInt(e)) // 轉型
}

範例 5:鏈式呼叫 and_then

fn read_and_double(path: &str) -> Result<i32, AppError> {
    std::fs::read_to_string(path)
        .map_err(AppError::from)               // 轉成 AppError
        .and_then(|s| s.trim().parse::<i32>()
            .map_err(AppError::from))          // 解析錯誤轉型
        .map(|n| n * 2)                         // 成功後乘二
}

常見陷阱與最佳實踐

陷阱 說明 建議的做法
忘記在 Result 函式中使用 ? 直接寫 let x = foo(); 會得到 Result,而非實際值,編譯錯誤。 使用 ?match 明確處理錯誤。
過度使用 unwrap() / expect() 在生產環境中會導致程式在錯誤時直接 panic,失去錯誤資訊。 僅在測試或確定不會失敗的情況下使用。
錯誤類型過於細碎 為每個函式都建立新錯誤列舉,導致 match 變得冗長。 使用共用的自訂錯誤(如 AppError),透過 #[from] 自動轉換。
忽略錯誤的上下文 只回傳底層錯誤訊息,缺少操作階段資訊,除錯困難。 在轉換錯誤時加入額外描述(thiserroranyhow::Context)。
async 函式中混用 Resultpanic! panic! 會直接終止執行緒,破壞非同步任務的錯誤傳播機制。 盡量把可恢復錯誤包成 Result,只在不可恢復的情況下使用 panic!

最佳實踐

  1. 統一錯誤型別:在 crate 根目錄定義一個 Error 列舉,所有模組都 #[from] 轉換。
  2. 盡量使用 ?:保持程式碼線性,避免過度嵌套 match
  3. 加入錯誤上下文:使用 anyhow::Context 或手動 map_err 加上描述。
  4. 在公共 API 前加上文件:說明會回傳哪些錯誤,讓使用者能正確 match
  5. 測試錯誤路徑:寫單元測試驗證 Err 情況的行為,避免隱藏 bug。

實際應用場景

1. 網路服務的請求處理

在一個 HTTP 伺服器中,每個請求可能會觸發檔案 I/O、資料庫查詢、JSON 解析等多種錯誤。透過 Result 的鏈式組合,我們可以把所有錯誤統一轉成 AppError,最後在中介層(middleware)一次性產生 HTTP 回應:

async fn handle_request(req: Request) -> Result<Response, AppError> {
    let body = hyper::body::to_bytes(req.into_body()).await?;
    let payload: MyPayload = serde_json::from_slice(&body)
        .map_err(|e| AppError::ParseInt(e.into()))?;
    let data = db::query(payload.id).await?;
    Ok(Response::new(serde_json::to_string(&data)?))
}

2. 命令列工具(CLI)

CLI 常需要把錯誤訊息直接印到標準錯誤,並回傳非零退出碼。anyhow 搭配 thiserror 能讓主函式只回傳 Result<(), AppError>main 只負責打印錯誤:

fn main() -> Result<(), AppError> {
    let args: Vec<String> = std::env::args().collect();
    let file = args.get(1).ok_or_else(|| AppError::ConfigMissing("missing file argument".into()))?;
    let content = read_file(file)?;
    println!("{}", content);
    Ok(())
}

3. 嵌入式系統的資源受限環境

no_std 環境下,我們仍然可以使用 Result,但錯誤型別通常會是自訂的 enum,避免使用 std::io::Error。透過 ?,即使在資源受限的情況下,也能保持錯誤傳遞的清晰度。


總結

Rust 的錯誤處理機制以 型別安全 為核心,透過 Result<T, E>? 運算子以及自訂錯誤列舉,讓開發者可以:

  • 明確 表達每個操作可能失敗的情況
  • 一致 地向上層傳播錯誤,讓呼叫者決定回應策略
  • 保持 程式碼的可讀性與可維護性

在實務開發中,遵循「統一錯誤型別 + 加入上下文」的原則,配合 thiserroranyhow 等輔助 crate,能大幅減少重複樣板,提升除錯效率。無論是 CLI、Web 服務或嵌入式應用,掌握錯誤傳播與處理的技巧都是寫出 可靠安全 Rust 程式的關鍵一步。祝你在 Rust 的旅程中,錯誤不再是阻礙,而是讓程式更堅韌的助力!