Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

断言与中止

Move 语言中的错误处理机制与大多数编程语言截然不同:它没有 try/catch 异常捕获机制。当出现错误时,交易要么完全成功,要么通过中止(abort)回滚所有状态变更。abort 用于立即中止执行,assert! 宏则提供了一种便捷的条件检查方式——当条件不满足时自动中止。

abort 关键字

基本用法

abort 是 Move 的关键字,用于立即停止当前交易的执行。它接受一个 u64 类型的错误码作为参数:

module book::abort_basic;

const ENotAllowed: u64 = 0;

public fun only_positive(value: u64): u64 {
    if (value == 0) {
        abort ENotAllowed
    };
    value
}

#[test, expected_failure(abort_code = ENotAllowed)]
fun abort_on_zero() {
    only_positive(0);
}

abort 被触发时,当前交易的所有状态变更都会被撤销,链上不会留下任何修改痕迹,但消耗的 gas 费不会退还。

abort 的语法形式

abort 可以作为表达式使用。由于它永远不会返回值,可以用在任何需要表达式的地方:

module book::abort_expr;

const EInvalidChoice: u64 = 0;

public fun describe(choice: u8): vector<u8> {
    if (choice == 1) {
        b"Option A"
    } else if (choice == 2) {
        b"Option B"
    } else {
        abort EInvalidChoice
    }
}

#[test]
fun describe_ok() {
    assert_eq!(describe(1), b"Option A");
    assert_eq!(describe(2), b"Option B");
}

assert! 宏

基本用法

assert! 是一个内置宏,它检查一个布尔条件,如果条件为 false,则以给定的错误码中止执行:

module book::assert_basic;

const ENotAuthorized: u64 = 0;
const EInvalidAmount: u64 = 1;

public fun transfer_tokens(
    sender: address,
    admin: address,
    amount: u64,
) {
    assert!(sender == admin, ENotAuthorized);
    assert!(amount > 0, EInvalidAmount);
    // 主要逻辑在这里...
}

#[test]
fun valid_transfer() {
    transfer_tokens(@0x1, @0x1, 100);
}

#[test, expected_failure(abort_code = ENotAuthorized)]
fun not_authorized() {
    transfer_tokens(@0x1, @0x2, 100);
}

#[test, expected_failure(abort_code = EInvalidAmount)]
fun invalid_amount() {
    transfer_tokens(@0x1, @0x1, 0);
}

assert! 本质上是 if (!condition) abort code 的语法糖,让代码更加简洁易读。

单参数 assert!

在测试中,assert! 可以只传一个参数,省略错误码。此时如果条件为 false,将以默认错误码中止:

module book::assert_single;

#[test]
fun assert_single_arg() {
    let x = 42;
    assert!(x == 42);       // 仅检查条件,无自定义错误码
    assert!(x > 0);
    assert!(x != 100);
}

错误常量约定

命名规范

Move 社区约定使用 E 前缀加大驼峰命名法(EPascalCase)来定义错误常量,类型统一为 u64

module book::error_conventions;

const ENotOwner: u64 = 0;
const EInsufficientBalance: u64 = 1;
const EItemNotFound: u64 = 2;
const EAlreadyExists: u64 = 3;
const EExpired: u64 = 4;

public fun check_owner(caller: address, owner: address) {
    assert!(caller == owner, ENotOwner);
}

public fun check_balance(balance: u64, required: u64) {
    assert!(balance >= required, EInsufficientBalance);
}

每个模块内的错误码通常从 0 开始递增,确保每个错误码在模块内是唯一的。

Move 2024 #[error] 属性

Move 2024 引入了 #[error] 属性,允许错误常量使用 vector<u8> 类型来提供人类可读的错误信息:

module book::error_attribute;

#[error]
const ECustomNotFound: vector<u8> = b"The requested item was not found";

#[error]
const EInvalidInput: vector<u8> = b"Input validation failed: value out of range";

public fun find_item(id: u64): u64 {
    if (id == 0) {
        abort ECustomNotFound
    };
    id
}

public fun validate(value: u64) {
    assert!(value <= 1000, EInvalidInput);
}

#[test, expected_failure(abort_code = ECustomNotFound)]
fun not_found() {
    find_item(0);
}

使用 #[error] 属性后,当交易失败时,Sui CLI 与 GraphQL 等工具会将 abort 码解码为可读信息(如 Error from '0x...::module::fun' (line N), abort 'EConstName': "message")。不写错误码assert!(cond)abort 也会自动从源码行号派生一个“clever error”码,便于定位;但若需稳定错误码(如测试中按码断言),应使用具名错误常量。

Clever Errors 的编码与解码

#[error] 的常量在运行时会被编码为一个 u64 的 clever 码,其高位包含:标记位(表示是 clever 码)、中止发生的行号常量名在模块标识表中的索引常量值在模块常量表中的索引。例如某次中止得到的十六进制码可能形如 0x8000_0007_0001_0000,解码后可得到行号、常量名(如 EIsThree)和常量值(如 b"The value is three"),工具会渲染为类似:

Error from '0x...::module::fun' (line 7), abort 'EIsThree': "The value is three"

未提供错误码assert!(cond)abort 也会生成 clever 码,其中“常量名索引”和“常量值索引”用哨兵值 0xffff 填充,仅行号有效,便于在源码中定位。
中的 assert!/abort 的行号取宏调用处,而不是宏定义内部,这样错误信息会指向调用方代码。

如需从 u64 手工解码 clever 码,可参考 Sui 文档或 CLI/GraphQL 的解码流程;日常开发中直接依赖 Sui CLI 与 GraphQL 的自动解码即可。

错误处理的最佳实践

前置断言模式

最佳实践是将所有的断言检查放在函数主逻辑之前,这样可以在执行任何状态变更前就发现问题,即“先验证,后执行“:

module book::abort_example;

const ENotAuthorized: u64 = 0;
const EInvalidAmount: u64 = 1;
const EInsufficientBalance: u64 = 2;

#[error]
const ECustomError: vector<u8> = b"This is a custom error message";

public fun transfer_tokens(
    sender: address,
    admin: address,
    amount: u64,
) {
    // 所有断言在前
    assert!(sender == admin, ENotAuthorized);
    assert!(amount > 0, EInvalidAmount);
    // 主要逻辑在后...
}

public fun must_be_positive(value: u64): u64 {
    if (value == 0) {
        abort EInvalidAmount
    };
    value
}

#[test]
fun assert_ok() {
    let result = must_be_positive(42);
    assert_eq!(result, 42);
}

#[test, expected_failure(abort_code = EInvalidAmount)]
fun abort_zero() {
    must_be_positive(0);
}

交易的原子性

由于 Move 没有 try/catch 机制,整个交易是原子性的:

  • 全部成功:所有操作都执行完毕,状态变更生效
  • 全部回滚:只要有一个 abort 被触发,所有状态变更都被撤销

这种设计简化了安全模型——开发者不需要担心部分执行导致的不一致状态:

module book::atomic_example;

const EStepOneFailed: u64 = 0;
const EStepTwoFailed: u64 = 1;

public fun multi_step_operation(a: u64, b: u64) {
    // 步骤一
    assert!(a > 0, EStepOneFailed);

    // 步骤二
    assert!(b > a, EStepTwoFailed);

    // 如果执行到这里,说明所有检查都通过了
    // 实际操作逻辑...
}

#[test]
fun success() {
    multi_step_operation(5, 10);
}

#[test, expected_failure(abort_code = EStepTwoFailed)]
fun step_two_fails() {
    // 即使步骤一通过了,步骤二失败也会回滚所有变更
    multi_step_operation(5, 3);
}

小结

Move 的错误处理机制简洁而强大。abort 立即中止执行并回滚所有状态变更,assert! 宏提供了简洁的条件检查语法。错误常量使用 E 前缀的驼峰命名,Move 2024 还引入了 #[error] 属性支持可读的错误信息。由于没有 try/catch 机制,交易具有完全的原子性——要么全部成功,要么全部回滚。最佳实践是将断言检查放在函数主逻辑之前,确保“先验证,后执行“。