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 能自動為列舉產生 Display 與 Error 實作。
範例 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_err、and_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] 自動轉換。 |
| 忽略錯誤的上下文 | 只回傳底層錯誤訊息,缺少操作階段資訊,除錯困難。 | 在轉換錯誤時加入額外描述(thiserror、anyhow::Context)。 |
在 async 函式中混用 Result 與 panic! |
panic! 會直接終止執行緒,破壞非同步任務的錯誤傳播機制。 |
盡量把可恢復錯誤包成 Result,只在不可恢復的情況下使用 panic!。 |
最佳實踐
- 統一錯誤型別:在 crate 根目錄定義一個
Error列舉,所有模組都#[from]轉換。 - 盡量使用
?:保持程式碼線性,避免過度嵌套match。 - 加入錯誤上下文:使用
anyhow::Context或手動map_err加上描述。 - 在公共 API 前加上文件:說明會回傳哪些錯誤,讓使用者能正確
match。 - 測試錯誤路徑:寫單元測試驗證
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>、? 運算子以及自訂錯誤列舉,讓開發者可以:
- 明確 表達每個操作可能失敗的情況
- 一致 地向上層傳播錯誤,讓呼叫者決定回應策略
- 保持 程式碼的可讀性與可維護性
在實務開發中,遵循「統一錯誤型別 + 加入上下文」的原則,配合 thiserror、anyhow 等輔助 crate,能大幅減少重複樣板,提升除錯效率。無論是 CLI、Web 服務或嵌入式應用,掌握錯誤傳播與處理的技巧都是寫出 可靠、安全 Rust 程式的關鍵一步。祝你在 Rust 的旅程中,錯誤不再是阻礙,而是讓程式更堅韌的助力!