suideveloper
All questions
Sui Move

How do abort codes and clever errors work in Sui Move?

November 6, 2026

When Move code aborts, through abort or a failed assert!, the whole transaction fails and nothing it did is applied. The failure reports a u64 abort code plus the module and function where it happened. Plain error constants (const ENotOwner: u64 = 0;) give you a numeric code you have to look up. Clever errors, constants marked #[error], can hold a message like b"Not the owner"; the compiler packs the source line and references to the constant into the u64, and the Sui CLI, GraphQL and SDKs decode it into a readable message.

Plain abort codes

Move
module book::vault;

const ENotOwner: u64 = 0;
const EInsufficient: u64 = 1;

public struct Vault has key {
    id: UID,
    owner: address,
    balance: u64,
}

public fun withdraw(v: &mut Vault, amount: u64, ctx: &TxContext) {
    assert!(v.owner == ctx.sender(), ENotOwner);
    assert!(v.balance >= amount, EInsufficient);
    // ...
}

assert!(cond, code) is shorthand for if (!cond) abort code. Conventions and facts:

  • Name error constants E + CamelCase.
  • Codes are local to a module: code 1 in your module and code 1 in sui::coin mean different things. Always read the code together with the module in the error.
  • There's no try/catch. An abort can't be caught by the caller; it fails the entire transaction, including every other command in a PTB. You still pay gas for the work done.

Clever errors

Move
module book::vault;

#[error]
const ENotOwner: vector<u8> = b"Only the vault owner can withdraw";

#[error(code = 1)]
const EInsufficient: vector<u8> = b"Not enough balance in the vault";

public struct Vault has key {
    id: UID,
    owner: address,
    balance: u64,
}

public fun withdraw(v: &mut Vault, amount: u64, ctx: &TxContext) {
    assert!(v.owner == ctx.sender(), ENotOwner);
    assert!(v.balance >= amount, EInsufficient);
}

The constant can be any constant type, though vector<u8> messages are the usual choice. At runtime the abort code is still a u64, laid out as:

Text
0x8000_0007_0001_0000
  |       |    |    +-- index of the constant's value
  |       |    +------- index of the constant's name
  |       +------------ source line of the abort
  +-------------------- tag bit: this is a clever error

Decoded, it looks like:

Text
Error from '0x42::vault::withdraw' (line 7), abort 'ENotOwner': "Only the vault owner can withdraw"

The optional #[error(code = n)] attaches a fixed u8 code, stored in its own field. The Sui framework uses this so each error has a stable number that doesn't depend on line numbers.

Aborts without a code

assert!(cond) and a bare abort are valid. The compiler derives a clever-error code holding the line number, with 0xffff in the name and value fields. That's fine for "should never happen" branches; for errors users can trigger, give a named constant.

Gotchas

  • Clever codes change when the file changes. Reformatting or adding a line moves the line number, so the u64 changes. Match errors by constant name (or the explicit code), never by the raw numeric value.
  • Macros report the call site. If an assert! inside a macro fails, the line is where the macro was invoked.
  • Old abort codes in clients. If your frontend maps numeric codes to messages, switching a constant to #[error] changes the number it receives.

Reading the error in TypeScript

With @mysten/sui v2, a failed transaction carries a structured error:

TypeScript
const result = await client.signAndExecuteTransaction({ signer, transaction: tx })

if (result.$kind === 'FailedTransaction') {
  const err = result.FailedTransaction.status.error
  if (err?.$kind === 'MoveAbort') {
    console.log(err.MoveAbort.abortCode, err.MoveAbort.location?.functionName)
    console.log(err.MoveAbort.cleverError?.constantName, err.MoveAbort.cleverError?.value)
  }
}

cleverError exposes constantName, value, lineNumber and errorCode when available. Simulating first shows the same error without spending gas; see dry runs and why a transaction failed.

Testing aborts

Move
#[test, expected_failure(abort_code = ENotOwner)]
fun non_owner_cannot_withdraw() { /* ... */ }

Clever error constants work in abort_code too. See unit tests.

Sources

More on sui move