在 (Day10) 我們理解了 Result
和 Option
的基礎概念。
今天我們要深入探討:如何在大型專案中建立完整的錯誤處理策略。
關鍵問題是:函式庫 (Library) 與應用程式 (Application) 的錯誤處理需求完全不同。
函式庫的使用者需要:
// 函式庫應該提供精確的錯誤型別
pub enum DatabaseError {
ConnectionFailed(String),
QueryFailed(String),
RecordNotFound,
DuplicateKey(String),
}
pub fn query_user(id: u64) -> Result<User, DatabaseError> {
// 使用者可以精確匹配錯誤類型
match execute_query(id) {
Ok(user) => Ok(user),
Err(e) => Err(DatabaseError::QueryFailed(e.to_string())),
}
}
應用程式的需求是:
// 應用程式可以用更簡單的方式
fn process_request(id: u64) -> Result<Response> {
let user = query_user(id)
.context("查詢使用者失敗")?;
let profile = fetch_profile(user.id)
.context("取得個人資料失敗")?;
Ok(Response { user, profile })
}
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("資料庫連線失敗: {0}")]
ConnectionFailed(String),
#[error("查詢失敗: {query}")]
QueryFailed { query: String },
#[error("找不到記錄 (id: {0})")]
RecordNotFound(u64),
#[error("重複的鍵值: {0}")]
DuplicateKey(String),
#[error(transparent)]
IoError(#[from] std::io::Error),
}
關鍵特性:
#[error("...")]
定義錯誤訊息#[from]
自動實作 From
trait#[transparent]
保留原始錯誤的 Displayuse thiserror::Error;
use std::io;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("設定檔不存在: {path}")]
FileNotFound { path: String },
#[error("設定檔格式錯誤: {0}")]
ParseError(String),
#[error("缺少必要欄位: {field}")]
MissingField { field: String },
#[error("IO 錯誤")]
Io(#[from] io::Error),
#[error("JSON 解析錯誤")]
Json(#[from] serde_json::Error),
}
pub struct Config {
pub host: String,
pub port: u16,
}
impl Config {
pub fn load(path: &str) -> Result<Self, ConfigError> {
// 讀取檔案
let content = std::fs::read_to_string(path)
.map_err(|_| ConfigError::FileNotFound {
path: path.to_string(),
})?;
// 解析 JSON
let json: serde_json::Value = serde_json::from_str(&content)?;
// 提取欄位
let host = json["host"]
.as_str()
.ok_or_else(|| ConfigError::MissingField {
field: "host".to_string(),
})?
.to_string();
let port = json["port"]
.as_u64()
.ok_or_else(|| ConfigError::MissingField {
field: "port".to_string(),
})? as u16;
Ok(Config { host, port })
}
}
// 使用者可以精確處理錯誤
fn handle_config() {
match Config::load("config.json") {
Ok(config) => println!("載入成功: {}:{}", config.host, config.port),
Err(ConfigError::FileNotFound { path }) => {
println!("找不到設定檔: {}", path);
// 建立預設設定
}
Err(ConfigError::MissingField { field }) => {
println!("缺少欄位: {}", field);
// 使用預設值
}
Err(e) => println!("其他錯誤: {}", e),
}
}
use anyhow::{Context, Result};
fn process_file(path: &str) -> Result<String> {
let content = std::fs::read_to_string(path)
.context("讀取檔案失敗")?;
let parsed = parse_content(&content)
.context("解析內容失敗")?;
let result = transform_data(parsed)
.context("轉換資料失敗")?;
Ok(result)
}
// 錯誤訊息會自動堆疊
// Error: 轉換資料失敗
// Caused by:
// 0: 解析內容失敗
// 1: 讀取檔案失敗
// 2: No such file or directory (os error 2)
關鍵特性:
Error
的型別都能轉成 anyhow::Error
.context()
添加錯誤語境use anyhow::{Context, Result, bail, ensure};
struct User {
id: u64,
name: String,
age: u32,
}
fn fetch_user(id: u64) -> Result<User> {
if id == 0 {
bail!("無效的使用者 ID: {}", id);
}
let user = database::query_user(id)
.context(format!("查詢使用者失敗 (id: {})", id))?;
ensure!(user.age >= 18, "使用者未滿 18 歲");
Ok(user)
}
fn process_users(ids: &[u64]) -> Result<Vec<User>> {
let mut users = Vec::new();
for &id in ids {
let user = fetch_user(id)
.context(format!("處理使用者 {} 時發生錯誤", id))?;
users.push(user);
}
Ok(users)
}
fn main() -> Result<()> {
let ids = vec![1, 2, 3];
let users = process_users(&ids)
.context("批次處理使用者失敗")?;
println!("成功處理 {} 個使用者", users.len());
Ok(())
}
// === 函式庫層 (library crate) ===
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StorageError {
#[error("儲存失敗: {0}")]
WriteFailed(String),
#[error("讀取失敗: {0}")]
ReadFailed(String),
}
pub fn save_data(data: &str) -> Result<(), StorageError> {
// 精確的錯誤型別
Err(StorageError::WriteFailed("磁碟已滿".to_string()))
}
// === 應用層 (application crate) ===
use anyhow::{Context, Result};
fn handle_request() -> Result<()> {
let data = "important data";
// anyhow 自動轉換 StorageError
save_data(data)
.context("儲存請求資料失敗")?;
Ok(())
}
use thiserror::Error;
// 底層:資料庫錯誤
#[derive(Error, Debug)]
pub enum DbError {
#[error("連線失敗")]
ConnectionFailed,
#[error("查詢失敗")]
QueryFailed,
}
// 中層:業務邏輯錯誤
#[derive(Error, Debug)]
pub enum BusinessError {
#[error("使用者不存在")]
UserNotFound,
#[error("權限不足")]
PermissionDenied,
#[error("資料庫錯誤")]
Database(#[from] DbError),
}
// 上層:API 錯誤
#[derive(Error, Debug)]
pub enum ApiError {
#[error("請求無效: {0}")]
InvalidRequest(String),
#[error("內部錯誤")]
Internal(#[from] BusinessError),
}
// 每一層都可以精確處理自己關心的錯誤
fn api_handler(user_id: u64) -> Result<Response, ApiError> {
let user = fetch_user(user_id)?; // BusinessError -> ApiError
Ok(Response { user })
}
use anyhow::{Context, Result};
// 可恢復的錯誤:返回 Result
fn try_fetch_cache(key: &str) -> Result<Option<String>> {
match cache::get(key) {
Ok(value) => Ok(Some(value)),
Err(e) if is_recoverable(&e) => {
log::warn!("快取讀取失敗,將從資料庫讀取: {}", e);
Ok(None)
}
Err(e) => Err(e).context("快取系統故障"),
}
}
// 不可恢復的錯誤:直接 panic
fn load_critical_config() -> Config {
Config::load("config.json")
.expect("無法載入設定檔,程式無法繼續執行")
}
fn main() -> Result<()> {
// 初始化階段:不可恢復
let config = load_critical_config();
// 執行階段:可恢復
match try_fetch_cache("user:123") {
Ok(Some(data)) => println!("從快取讀取: {}", data),
Ok(None) => println!("快取未命中,從資料庫讀取"),
Err(e) => println!("快取系統故障: {}", e),
}
Ok(())
}
use anyhow::{Context, Result};
use std::collections::HashMap;
fn process_batch(items: &[Item]) -> Result<()> {
let mut errors = HashMap::new();
for (index, item) in items.iter().enumerate() {
if let Err(e) = process_item(item)
.context(format!("處理第 {} 個項目失敗", index + 1))
{
errors.insert(index, e);
}
}
if !errors.is_empty() {
log::error!("批次處理完成,{} 個項目失敗", errors.len());
for (index, error) in errors {
log::error!("項目 {}: {:?}", index, error);
}
}
Ok(())
}
use tracing::{error, info, instrument};
use anyhow::{Context, Result};
#[instrument(skip(data), fields(data_len = data.len()))]
fn save_to_database(id: u64, data: &[u8]) -> Result<()> {
info!("開始儲存資料");
database::write(id, data)
.context("寫入資料庫失敗")
.map_err(|e| {
error!("資料庫寫入錯誤: {:?}", e);
e
})?;
info!("資料儲存成功");
Ok(())
}
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Serialize, Deserialize)]
pub enum ApiError {
#[error("找不到資源: {resource_type} (id: {id})")]
NotFound {
resource_type: String,
id: u64,
},
#[error("驗證失敗: {message}")]
ValidationError {
message: String,
field: String,
},
}
// 可以轉成 JSON 回傳給前端
fn handle_api_error(error: ApiError) -> String {
serde_json::to_string(&error).unwrap()
}
use thiserror::Error;
use std::backtrace::Backtrace;
#[derive(Error, Debug)]
pub enum DetailedError {
#[error("操作失敗: {message}")]
OperationFailed {
message: String,
operation: String,
timestamp: u64,
backtrace: Backtrace,
},
}
impl DetailedError {
pub fn new(message: impl Into<String>, operation: impl Into<String>) -> Self {
DetailedError::OperationFailed {
message: message.into(),
operation: operation.into(),
timestamp: current_timestamp(),
backtrace: Backtrace::capture(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_conversion() {
let io_error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
);
let config_error: ConfigError = io_error.into();
match config_error {
ConfigError::Io(_) => (),
_ => panic!("錯誤轉換失敗"),
}
}
#[test]
fn test_error_message() {
let error = ConfigError::MissingField {
field: "host".to_string(),
};
assert_eq!(error.to_string(), "缺少必要欄位: host");
}
}
thiserror
提供精確的錯誤型別anyhow
快速開發和添加語境.context()
說明錯誤發生的位置Result
,讓呼叫者決定panic!
或 .expect()
關鍵洞察:好的錯誤處理不是捕捉所有錯誤,而是在正確的層級用正確的方式處理正確的錯誤。
在下一篇中,我們將探討 模組與可見性,討論如何縮小 API 表面,封裝不變式。