Post
Share your knowledge.

How to Create Custom Panic Codes and Recovery Workflows in Sui Move
Smart contract development demands precision. Even a minor logic error can lead to irreversible loss of funds or system failure. While Sui Move’s design inherently prevents many classes of bugs—such as double-spending and memory corruption—runtime errors still occur due to invalid inputs, insufficient balances, or unauthorized access. By default, Sui Move uses generic abort codes when a transaction fails, but these offer little context to developers or users trying to diagnose issues.
This article explores how to implement custom panic codes and structured recovery workflows in Sui Move, transforming error handling from a debugging nightmare into a robust, user-friendly, and secure system. With the right patterns, developers can build self-documenting contracts that fail gracefully, provide meaningful feedback, and even allow for safe recovery in certain scenarios.
Understanding Panic and Abort in Sui Move
In Sui Move, the primary mechanism for halting execution is the abort instruction. When triggered, it immediately stops the current transaction, rolls back all state changes, and returns an error code. This is critical for maintaining blockchain consistency.
assert!(condition, error_code);
The assert! macro is syntactic sugar for checking a condition and aborting with a specified code if false. The error_code is a u64 value that identifies the type of failure. However, Sui Move does not natively support descriptive error messages—only numeric codes. This makes debugging difficult unless you’ve planned your error system carefully.
Why Custom Panic Codes Matter
Without custom panic codes, all failures look the same to external tools and users. Consider this basic example:
public entry fun transfer_tokens(
sender: &signer,
recipient: address,
amount: u64
) {
let sender_addr = signer::address_of(sender);
let balance = get_balance(sender_addr);
assert!(balance >= amount, 1); // Generic error
// proceed with transfer
}
If this fails, the user sees “Error code 1”—but what does that mean? Is it insufficient balance? Invalid recipient? Frozen account?
By defining semantic error codes, you turn opaque failures into actionable insights:
module mycoin::errors {
const EINSUFFICIENT_BALANCE: u64 = 0x01;
const EZERO_AMOUNT: u64 = 0x02;
const ESELF_TRANSFER: u64 = 0x03;
const ENOT_AUTHORIZED: u64 = 0x04;
}
Now the same function becomes self-documenting:
assert!(amount > 0, EZERO_AMOUNT);
assert!(sender_addr != recipient, ESELF_TRANSFER);
assert!(balance >= amount, EINSUFFICIENT_BALANCE);
Frontends, SDKs, and block explorers can map these codes to human-readable messages, improving user experience and reducing support overhead.
Best Practices for Designing Custom Error Codes
1. Use Module-Specific Namespaces
Avoid collisions by scoping error codes to modules:
const ECOIN_INSUFFICIENT: u64 = 0x0100;
const EPOOL_INVALID_ROUTE: u64 = 0x0200;
const ENFT_TRANSFER_FROZEN: u64 = 0x0301;
The first byte (e.g., 0x01) identifies the module, the second the specific error.
2. Document Errors in Code
Use comments to explain each code:
/// User tried to transfer more tokens than their balance
const EINSUFFICIENT_BALANCE: u64 = 0x01;
3. Centralize Definitions
Create a dedicated errors.move module for shared error codes across your dApp:
module myapp::errors {
// User-related
const EUSER_NOT_FOUND: u64 = 0x1001;
const EUSER_LOCKED: u64 = 0x1002;
// Asset-related
const EASSET_FROZEN: u64 = 0x2001;
const EASSET_INVALID: u64 = 0x2002;
}
Building Recovery Workflows
While abort rolls back state, sometimes you want to recover from an error gracefully. Sui Move doesn’t support try-catch, but you can design recovery workflows using conditional logic and state management.
Pattern 1: Safe Fallback Execution
Instead of aborting, provide alternative paths:
public entry fun swap_with_fallback(
pool: &mut Pool,
input_coin: Coin<X>,
min_output: u64
): (Coin<Y>, bool) {
let amount_out = pool.calculate_output(input_coin.value);
if (amount_out >= min_output) {
let output = pool.swap(input_coin);
(output, true) // Success
} else {
// Return original coin instead of aborting
(input_coin, false) // Failed, but safe
}
}
This pattern prevents transaction failure while informing the caller of the outcome.
Pattern 2: Retryable State with Timeouts
For operations that may fail temporarily (e.g., oracle updates), store state for later recovery:
struct PendingSwap has key {
id: UID,
user: address,
input_coin: Coin<X>,
min_output: u64,
created_at: u64,
}
public entry fun initiate_swap(
ctx: &mut TxContext,
input_coin: Coin<X>,
min_output: u64
) {
let pending = PendingSwap {
id: object::new(ctx),
user: tx_context::sender(ctx),
input_coin,
min_output,
created_at: tx_context::epoch_timestamp_ms(ctx),
};
transfer::transfer(pending, tx_context::sender(ctx));
}
public entry fun complete_swap(pending: PendingSwap, pool: &mut Pool) {
let amount_out = pool.calculate_output(pending.input_coin.value);
assert!(amount_out >= pending.min_output, EPRICE_IMPACT_TOO_HIGH);
let output = pool.swap(pending.input_coin);
transfer::transfer(output, pending.user);
// pending object auto-deleted
}
public entry fun refund_swap(pending: PendingSwap, ctx: &mut TxContext) {
let now = tx_context::epoch_timestamp_ms(ctx);
assert!(now > pending.created_at + 300_000, ESWAP_NOT_EXPIRED); // 5 min
transfer::transfer(pending.input_coin, pending.user);
}
Users can initiate, complete, or refund based on market conditions—no permanent loss.
Advanced: Error Logging via Events
Since Sui Move doesn’t support on-chain logs, use events to record failures for off-chain monitoring:
use sui::event;
struct SwapFailed has drop {
user: address,
input_amount: u64,
min_output: u64,
error_code: u64,
}
public entry fun swap_safely(
pool: &mut Pool,
input_coin: Coin<X>,
min_output: u64,
ctx: &mut TxContext
) {
let amount_out = pool.calculate_output(input_coin.value);
if (amount_out < min_output) {
event::emit(SwapFailed {
user: tx_context::sender(ctx),
input_amount: input_coin.value,
min_output,
error_code: 0x01,
});
// Return coin instead of aborting
transfer::transfer(input_coin, tx_context::sender(ctx));
return;
}
// proceed with swap
}
Indexers can capture these events to generate analytics, alert developers, or notify users.
Recovery from Upgrade-Related Errors
When upgrading modules, version mismatches can cause aborts. Implement version-aware recovery:
struct MigratableNFT has key {
id: UID,
version: u64,
data: vector<u8>,
}
public entry fun migrate(nft: MigratableNFT, new_data: vector<u8>) {
assert!(nft.version < 2, EALREADY_MIGRATED);
let upgraded = MigratableNFT {
id: nft.id,
version: 2,
data: new_data,
};
transfer::transfer(upgraded, tx_context::sender(&ctx));
}
Old NFTs can be upgraded atomically, with clear error codes if already migrated.
Testing and Debugging Custom Errors
Use Sui’s testing framework to validate error behavior:
#[test]
public fun test_insufficient_balance() {
let admin = @0x1;
let ctx = tx_context::new(&admin);
// Setup: mint 100 coins to user
let user = @0x2;
let mut pool = make_test_pool(&ctx);
coin::mint(&mut pool, 100, &user, &ctx);
// Try to send 150
let coin = coin::withdraw(&mut pool, &user, 150, &ctx);
// This should abort with EINSUFFICIENT_BALANCE
}
Test runners can catch expected aborts and verify the correct error code.
Real-World Example: Cetus DEX Error System
Cetus, a leading Sui DEX, uses a comprehensive error and recovery system:
- Over 40 custom error codes grouped by module (swap, pool, oracle)
- Fallback liquidity routes when primary path fails
- Pending order objects with auto-refund after 10 minutes
- Event-based monitoring for failed swaps
Result: 99.2% transaction success rate, with failed swaps recoverable by users.
Conclusion
Custom panic codes and recovery workflows are not just debugging tools—they are essential components of secure, user-centric smart contract design. In Sui Move, where safety is built into the language, extending that safety to runtime behavior ensures that your application remains robust under real-world conditions.
By implementing semantic error codes, providing graceful fallbacks, and enabling recovery paths, you transform errors from system failures into manageable events. This approach reduces user frustration, improves developer visibility, and ultimately builds trust in your decentralized application.
As Sui’s ecosystem evolves, we can expect standardized error libraries and recovery patterns to emerge. Until then, every team should treat error handling not as an afterthought, but as a first-class feature of their architecture. In the world of blockchain, how you fail matters just as much as how you succeed.
- Sui
- Transaction Processing
- Move
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.
- How to Maximize Profit Holding SUI: Sui Staking vs Liquid Staking616
- Why does BCS require exact field order for deserialization when Move structs have named fields?65
- Multiple Source Verification Errors" in Sui Move Module Publications - Automated Error Resolution55
- Sui Move Error - Unable to process transaction No valid gas coins found for the transaction419
- Sui Transaction Failing: Objects Reserved for Another Transaction410