Skip to main content

更好的錯誤處理 (Better Error Handling)

每當執行遇到中止時,交易會失敗並將中止代碼回傳給呼叫者。 Move VM 會回傳中止交易的模組名稱與中止代碼。這種行為對交易的呼叫者而言並非完全透明,特別是當單一函式包含多個可能會中止的相同函式呼叫時。在這種情況下,呼叫者將無法得知是哪一次呼叫中止了交易,這會導致除錯困難,或者難以向使用者提供有意義的錯誤訊息。

module book::module_a;

use book::module_b;

public fun do_something() {
let field_1 = module_b::get_field(1); // 可能會以 0 中止
/* ... 大量邏輯 ... */
let field_2 = module_b::get_field(2); // 可能會以 0 中止
/* ... 更多邏輯 ... */
let field_3 = module_b::get_field(3); // 可能會以 0 中止
}

上述範例說明了單一函式包含多個可能中止的呼叫的情況。如果 do_something 函式的呼叫者收到中止代碼 0,將很難理解是哪一次對 module_b::get_field 的呼叫中止了交易。為了嘗試解決這個問題,有一些常見的模式可以用來改善錯誤處理。

規則 1:處理所有可能的情境 (Rule 1: Handle All Possible Scenarios)​

提供一個安全的「檢查」函式被認為是良好的實踐,該函式會回傳布林值,以指示是否能安全地執行操作。如果 module_b 提供了一個 has_field 函式來回傳欄位是否存在的情形(布林值),那麼 do_something 函式可以改寫如下:

module book::module_a;

use book::module_b;

const ENoField: u64 = 0;

public fun do_something() {
assert!(module_b::has_field(1), ENoField);
let field_1 = module_b::get_field(1);
/* ... */
assert!(module_b::has_field(2), ENoField);
let field_2 = module_b::get_field(2);
/* ... */
assert!(module_b::has_field(3), ENoField);
let field_3 = module_b::get_field(3);
}

透過在每次呼叫 module_b::get_field 之前加入自訂檢查,module_a 的開發者就能掌控錯誤處理,這也允許實作第二個規則。

規則 2:使用不同的代碼中止 (Rule 2: Abort with Different Codes)​

一旦由呼叫者模組處理中止代碼後,第二個技巧是針對不同的情境使用不同的中止代碼。透過這種方式,呼叫者模組可以向使用者提供有意義的錯誤訊息。module_a 可以改寫如下:

module book::module_a;

use book::module_b;

const ENoFieldA: u64 = 0;
const ENoFieldB: u64 = 1;
const ENoFieldC: u64 = 2;

public fun do_something() {
assert!(module_b::has_field(1), ENoFieldA);
let field_1 = module_b::get_field(1);
/* ... */
assert!(module_b::has_field(2), ENoFieldB);
let field_2 = module_b::get_field(2);
/* ... */
assert!(module_b::has_field(3), ENoFieldC);
let field_3 = module_b::get_field(3);
}

現在,呼叫者模組可以向使用者提供有意義的錯誤訊息。如果呼叫者收到中止代碼 0,它可以被轉譯為「欄位 1 不存在」。如果呼叫者收到中止代碼 1,它可以被轉譯為「欄位 2 不存在」。以此類推。

規則 3:回傳 bool 而不是 assert (Rule 3: Return bool Instead of assert)​

開發者經常會忍不住去新增一個公開函式來斷言所有條件並中止執行。然而,建立一個回傳布林值的函式是更好的實踐。透過這種方式,呼叫者模組可以處理錯誤並向使用者提供有意義的錯誤訊息。

module book::some_app_assert;

const ENotAuthorized: u64 = 0;

public fun do_a() {
assert_is_authorized();
// ...
}

public fun do_b() {
assert_is_authorized();
// ...
}

/// 不要這樣做
public fun assert_is_authorized() {
assert!(/* 某個條件 */ true, ENotAuthorized);
}

這個模組可以改寫如下:

module book::some_app;

const ENotAuthorized: u64 = 0;

public fun do_a() {
assert!(is_authorized(), ENotAuthorized);
// ...
}

public fun do_b() {
assert!(is_authorized(), ENotAuthorized);
// ...
}

public fun is_authorized(): bool {
/* 某個條件 */ true
}

// 私有函式仍然可以用於避免在多個地方使用相同中止代碼的相同條件時發生程式碼重複
fun assert_is_authorized() {
assert!(is_authorized(), ENotAuthorized);
}

善用這三個規則將使交易的錯誤處理對呼叫者而言更加透明,並允許其他開發者在其模組中使用自訂的中止代碼。