Skip to content

Error Handling and Return Codes

View as Markdown

Use this page when you need to decide how a Rust program reports success or failure, and how to read the codes it gets back.

An entry function returning Result<u64, u64> ends the program through the exit syscall:

ReturnEffect
Ok(code)Exit with code, no revert. State changes are kept.
Err(code)Exit with code and the revert flag set. State changes are discarded.

Exiting explicitly does the same thing and never returns:

use thru_core::program_utils;
program_utils::succeed(0);
program_utils::revert(7);

Choose your own error codes. Keep them stable, because clients and indexers surface the raw number.

use thru_core::{assert_eq_or_revert, assert_or_revert, revert};
assert_or_revert!(amount > 0, error = 3);
assert_eq_or_revert!(account.owner(), &expected_owner, error = 4);
revert!(error = 5);

Without an explicit error =, assert_or_revert! and assert_eq_or_revert! revert with code 4444.

thru-core installs a panic handler that logs the panic message with tvm_println! and exits with revert code PANIC_ERROR_CODE (555). Anything that panics in Rust — slice bounds, unwrap, arithmetic overflow in debug profiles — therefore surfaces as 555, not as your own code. Return Err explicitly for expected failures so callers get a meaningful code.

AccountManager panics deliberately in two situations: mutating an account that is currently borrowed, and invoking another program while any borrow is live.

Syscall wrappers and AccountManager mutators return SyscallCode. Success is 0, SuccessExit is 1, and every error is negative:

VariantValue
BadSegmentTableSize-7
InvalidAccountIndex-8
AccountDoesNotExist-9
AccountNotWritable-10
BalanceOverflow-11
AccountTooBig-12
InvalidObjRefKind-13
ObjNotWritable-14
AccountAlreadyExists-15
BadAccountAddress-16
AccountIsNotProgram-17
AccountHasData-18
SegmentAlreadyMapped-19
BadParams-20
InvalidSegmentId-21
InvalidAddress-22
InvalidStateProof-23
CallDepthTooDeep-24
Revert-25
InsufficientPages-26
InvalidAccount-27
InvalidSegmentSize-28
UnfreeablePage-29
LogDataTooLarge-30
EventTooLarge-31
InvalidProofLen-32
InvalidProofSlot-33
AccountInCompressionTimeout-34
InvalidAccountDataSize-35
InvalidSeedLength-36
TxnHasCompressedAccount-37
InsufficientBalance-38
InvalidOffset-39
ComputeUnitsExceeded-40
InvalidFlags-41
EphemeralAccountCannotCreatePersistent-42
AccountCompressionNotAllowed-43
InvalidSignature-44
AccountUncompressable-45
EventIsZeroSize-46
StateBytesAddedOverflow-47
StateBytesRemovedOverflow-48
ProgramInCallStack-49

Any value the SDK does not recognize maps to UnknownCode. SyscallCode converts to and from i64, so propagating one as a revert code looks like:

if code != SyscallCode::Success {
program_utils::revert(code as i64 as u64);
}

AccountManager::from_txn, get, get_readonly, and account_role return Result<_, AccountError>:

VariantMeaning
IndexOutOfBounds { index, max }The index is past the transaction’s account count.
AlreadyBorrowedMutably { index }An immutable borrow was requested while a mutable borrow is live.
AlreadyBorrowed { index }A mutable borrow was requested while another borrow is live.
TooManyAccountsAccessed { max_capacity }More than NUM_ACCOUNTS distinct accounts were touched. Raise the const generic.
UnsupportedTxnVersion { version }The transaction is not version 1.
InfoAccessFailed { index, err }The account metadata or data could not be read; err is a MemoryError.

thru_core::mem accessors return MemoryError::UnsupportedAccountVersion { version } when account metadata is not version 1.

CodeSource
555Rust panic handler.
4444Default code for assert_or_revert! and assert_eq_or_revert!.
0xBAD0A170InvokeAuth magic mismatch.
0xBAD0A171Delegated account is not owned by the calling program.
0xBAD0A173InvokeAuth account index out of range.
0xBAD0A174Authorization check found a delegation whose authorizing program does not own the account.

Avoid reusing these for your own errors.