上一篇看到 Option<T> 用 enum 表達「可能沒值」。這一篇的主角 Result<T, E> 用同一套 enum 設計表達「可能失敗」。Rust 沒有 try / catch,錯誤就是普通的回傳值,再配上 ? 運算子讓錯誤傳遞變得乾淨——這篇要把整套錯誤處理觀念講清楚。
Rust 的錯誤分類
Rust 把錯誤分成兩類:
| 類型 | 機制 | 用途 |
|---|---|---|
| 可恢復錯誤 | Result<T, E> | 預期可能發生、呼叫方有機會處理(檔案不存在、parse 失敗、網路逾時) |
| 不可恢復錯誤 | panic! | 程式邏輯出錯、進入不該到的狀態(陣列越界、unwrap 一個 None) |
沒有 try / catch。錯誤就是普通的回傳值,必須在型別系統裡明確處理。
panic!
panic! 是不可恢復的崩潰:預設會印出錯誤訊息、展開 stack、終止程式(或執行緒)。
fn main() {
panic!("something went terribly wrong");
}
執行:
thread 'main' panicked at src/main.rs:2:5:
something went terribly wrong
note: run with `RUST_BACKTRACE=1` for a backtrace
常見會 panic 的操作:
let v = vec![1, 2, 3];
let x = v[99]; // panic: index out of bounds
let n: i32 = "abc".parse().unwrap(); // panic: ParseIntError
函式庫程式碼盡量不要 panic,而是回傳 Result,把決定權交給呼叫方。
panic 適合在「程式遇到不可能發生的狀態」時用,例如違反不變式(invariant)。
Result<T, E>
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok 帶成功的值,Err 帶錯誤資訊。例子:
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => println!("opened: {file:?}"),
Err(e) => println!("failed: {e}"),
}
}
也可以按錯誤種類分流:
use std::fs::File;
use std::io::ErrorKind;
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(e) => match e.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("create failed: {e}"),
},
other => panic!("open failed: {other:?}"),
},
};
巢狀很醜,後面會用 ? 拍平。
unwrap、expect、unwrap_or 系列
開發、原型階段常用的快捷方法:
let f = File::open("a.txt").unwrap();
// Ok → 取出值;Err → panic(訊息固定不太友善)
let f = File::open("a.txt").expect("a.txt should exist");
// Err → panic 並印出自訂訊息(debug 比較有用)
let n: i32 = "abc".parse().unwrap_or(0);
// Err → 用 0 當預設值
let n: i32 = "abc".parse().unwrap_or_else(|_| -1);
// 同上但預設值由閉包產生(lazy)
let n: i32 = "abc".parse().unwrap_or_default();
// 用該型別的 Default::default()
正式程式碼避免亂 unwrap。每個 unwrap 都是在說「我跟編譯器保證這裡一定不會錯,否則就讓它崩」。
? 運算子:拍平錯誤傳遞
? 是 Rust 錯誤處理的命脈。它做的事:
- 如果是
Ok(v)→ 取出v繼續執行 - 如果是
Err(e)→ 直接從目前函式 return 那個錯誤
對比寫法:
use std::fs::File;
use std::io::{self, Read};
// 不用 ?
fn read_username() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
// 用 ?
fn read_username() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// 鏈式更短
fn read_username() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
? 也能用在 Option 上,意義對應:Some 取值、None 直接 return None。
限制:? 只能在回傳 Result / Option 的函式裡用
fn main() {
let f = File::open("a.txt")?; // ❌ main 預設回傳 ()
}
修法 1:讓 main 回傳 Result:
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("a.txt")?;
Ok(())
}
修法 2:把邏輯抽到一個回傳 Result 的函式,main 只負責呼叫它然後 unwrap。
自訂錯誤型別
實務上一個函式常會遇到不只一種錯誤,可以定義一個 enum 把它們包起來:
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
NotFound,
}
// 實作 From,讓 ? 自動轉型
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
fn read_number() -> Result<i32, AppError> {
use std::fs::read_to_string;
let s = read_to_string("num.txt")?; // io::Error → AppError::Io
let n: i32 = s.trim().parse()?; // ParseIntError → AppError::Parse
Ok(n)
}
? 的完整行為其實是:失敗時取出 Err(e),把 e 用 From 轉成函式回傳型別需要的錯誤,再 return。
真的在寫應用程式,不用每次都自己刻 From。社群有兩個常用 crate:
thiserror:給 library 用,巨集自動產生Display/Error/Fromanyhow:給 application 用,提供anyhow::Result<T>=Result<T, anyhow::Error>,能吃下任何錯誤,配?用爽
Option 與 Result 互轉
let opt: Option<i32> = Some(5);
let res: Result<i32, &str> = opt.ok_or("no value");
let res: Result<i32, &str> = Ok(5);
let opt: Option<i32> = res.ok();
常用方法速查
Result<T, E> 上的常用方法(Option<T> 大致對應):
| 方法 | 行為 |
|---|---|
.is_ok() / .is_err() | 判斷類型 |
.ok() / .err() | 轉成 Option<T> / Option<E> |
.unwrap() | 取值或 panic |
.expect("msg") | 取值或 panic(自訂訊息) |
.unwrap_or(default) | 取值或回傳預設 |
.unwrap_or_else(|e| ...) | 取值或執行 closure |
.map(|v| ...) | 對 Ok(v) 做變換 |
.map_err(|e| ...) | 對 Err(e) 做變換 |
.and_then(|v| ...) | flatMap,串接會回傳 Result 的操作 |
? | 取值或往外拋 |
何時 panic vs 何時 Result
判準很簡單:
- panic:bug、不變式被破壞、不該到達的狀態(用
unreachable!()、unimplemented!()、todo!()、assert!) - Result:可預期的失敗(IO、parse、網路、使用者輸入)
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("divide by zero") // 可預期,回傳 Err
} else {
Ok(a / b)
}
}
fn first_element<T>(v: &[T]) -> &T {
assert!(!v.is_empty()); // 違反前提條件,panic
&v[0]
}
Result + ? 串完,下一篇進集合(Vec、HashMap、String 操作)。那些方法多半回傳 Option 或 Result,會一直用到這篇的工具。
Latest Updates
- 2026.06.11 Content updated
