中止執行 (Aborting Execution)
一筆交易會以兩種方式之一結束:要麼成功,且其所做的所有變更都會套用並提交至區塊鏈;要麼會 中止,且不會套用任何變更。兩者之間沒有其他可能:交易無法部分成功,而深度巢狀函式呼叫中的中止會使整筆交易失敗。這種全有或全無的模型讓 Move 中的錯誤處理變得簡單且可預測——函式永遠不需要復原其變更,因為中止會一次復原所有內容。
Move 中沒有 catch 機制。中止無法被攔截或從中復原:它總是會讓整筆交易失敗。這是一項設計選擇——以彈性換取簡潔,並使部分更新的狀態不可能發生。
本節將介紹 Move 提供的中止工具:abort 運算式、assert! 巨集,以及定義錯誤碼與錯誤訊息的慣例。
中止 (Abort)
abort 關鍵字會立即停止執行。它通常會指定一個 中止碼——型別為 u64 的整數——並與中止之模組的身分一併回傳給交易的呼叫端。以下是一個範例:
let user_has_access = false;
// abort with a predefined constant if `user_has_access` is false
if (!user_has_access) {
abort 1
};
上述程式碼當然會以中止碼 1 中止。
中止碼有兩項特性,值得及早牢記:
- 中止碼是 模組區域 的。兩個不同模組都可以使用碼 1 中止,但它們代表不同意義;呼叫端必須將該碼與產生它的模組一併解讀。
- 中止碼不帶有訊息。區塊鏈只記錄數值碼與中止位置——讓這些碼易於理解是模組作者的責任,這正是下方的錯誤常數與錯誤訊息用途所在。
省略中止碼 (Omitting the Abort Code)
原始碼中可以省略中止碼——單獨的 abort 運算式是有效的 Move 程式碼:
// `abort` can also be used without an explicit abort code.
abort
不過,省略不代表不存在:呼叫端仍會收到一個由編譯器自動推導的 u64 中止碼。推導出的碼使用下方錯誤訊息所述的 clever-error 編碼——它包含模組與失敗所在的原始碼行,而常數名稱與值則留空。
這種形式有時稱為 乾淨中止,很適合根本不應可達的分支——例如涵蓋不可能出現值的 match 運算式萬用字元分支(請參閱 列舉與 Match)。由於推導出的碼會指向失敗位置,卻不說明其 意義,對於外部呼叫端實際可以觸發的條件,應優先使用明確的碼或錯誤訊息。
斷言巨集 (assert!)
assert! 巨集是內建巨集,用來檢查條件,並在條件為 false 時中止。它是原本需手動撰寫的 if + abort 組合之簡寫,也是 Move 程式碼中最常見的中止方式。第一個引數是條件;第二個選用引數是中止碼——省略時,會如同單獨的 abort 一樣自動推導一個碼:
// aborts if `user_has_access` is `false` with abort code 0
assert!(user_has_access, 0);
// expands to:
if (!user_has_access) {
abort 0
};
// the abort code can be omitted
assert!(user_has_access);
常見做法是在函式開頭放置斷言——先檢查所有條件,再進行變更。由於中止會還原整筆交易,這並非安全性所必需;但它能讓函式的需求一目了然,並避免將必然捨棄的工作浪費gas。
錯誤常數 (Error Constants)
像 assert!(user_has_access, 1) 這樣的原始數值碼,無法告訴讀者哪裡出錯。為了讓錯誤碼具有描述性,最佳做法是將其定義為常數。錯誤常數遵循專屬命名慣例——以 E 加上 CamelCase 描述組成——使其與一般的 ALL_CAPS 常數有所區別:
/// Error code for when the user has no access.
const ENoAccess: u64 = 0;
/// Trying to access a field that does not exist.
const ENoField: u64 = 1;
/// Updates a record.
public fun update_record(/* ... , */ user_has_access: bool, field_exists: bool) {
// asserts are way more readable now
assert!(user_has_access, ENoAccess);
assert!(field_exists, ENoField);
/* ... */
}
錯誤常數是一般的 u64 常數,編譯器不會對它們作任何特殊處理。不過,遵循此慣例可讓程式碼自我說明——assert!(user_has_access, ENoAccess) 讀起來就像一句話——而收到中止碼的呼叫端也能在模組的原始碼中找到相符的常數。撰寫良好的模組會為其可能產生的每種中止情境定義錯誤常數。
錯誤訊息 (Error Messages)
Move 2024 引入了 clever errors——以 #[error] 屬性標記的錯誤常數。不同於一般錯誤常數,它們可以是任何型別——最有用的是儲存人類可讀錯誤訊息的 vector<u8>:
#[error]
const ENotAuthorized: vector<u8> = "The user is not authorized to perform this action";
#[error]
const EValueTooLow: vector<u8> = "The value is too low, it should be at least 10";
/// Performs an action on behalf of the user.
public fun update_value(user: &mut User, value: u64) {
assert!(user.is_authorized, ENotAuthorized);
assert!(value >= 10, EValueTooLow);
user.value = value;
}
此屬性不會改變中止的本質:交易仍會以 u64 中止碼失敗。改變的是該碼的內容——編譯器會將中止的原始碼行號(對於 assert! 這類巨集,則是呼叫位置所在行)以及常數名稱和值的參考打包至其中。能理解此格式的工具——Sui CLI、瀏覽器、SDK——會將其解包並顯示完整資訊,例如:
來自 'book::assert_abort::update_value'(第 15 行)的錯誤,中止 'EValueTooLow':
「值過低,至少應為 10」
錯誤訊息免除了查詢數值碼意義的需求,這在面向公眾的應用程式中特別重要,因為閱讀失敗訊息的人通常不是模組作者。此編碼的另一面是,clever 中止碼的數值取決於原始碼版面配置:重新格式化模組或新增一行都會改變它。請依名稱參考這些常數——切勿依其編譯後的數值參考。編碼的確切版面配置請參閱 Move 參考文件中的 Clever Errors。
測試中的中止 (Aborts in Tests)
中止和其他行為一樣值得測試。#[expected_failure] 屬性會標記預期應中止的測試,而其 abort_code 引數會斷言確切的碼——若函式成功或以不同碼中止,測試便會失敗。我們會在測試章節中更詳細介紹此屬性。
延伸閱讀 (Further Reading)
- Move 參考文件中的 Abort and Assert。
- Move 參考文件中的 Clever Errors。
- 建議閱讀 Better Error Handling 指南,了解 Move 錯誤處理的最佳實務。