更好的错误处理
当执行过程中遇到中止时,交易将失败,并将中止代码返回给调用者。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
,将很难理解是哪个调用中止了交易。为了解决这个问题,可以使用一些常见的模式来改进错误处理。
规则 1:处理所有可能的情况
提供一个安全的“检查”函数以返回布尔值,指示操作是否可以安全执行是一种良好的实践。如果 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:使用不同的代码中止
一旦调用者模块处理了中止代码,使用不同的中止代码针对不同的情况。这使得调用者模块可以向用户提供有意义的错误消息。可以将 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:返回布尔值而不是断言
开发人员常常倾向于添加一个公共函数来断言所有条件并中止执行。然而,更好的做法是创建一个返回布尔值的函数。这样,调用者模块可以处理错误并向用户提供有意义的错误消息。
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);
}
}
利用这三个规则可以使交易调用者的错误处理更加透明,并允许其他开发人员在其模块中使用自定义中止代码。