---
title: Error Handling and Return Codes
description: "Return, revert, and interpret error codes in Rust programs:
  SyscallCode, AccountError, MemoryError, and the revert macros."
source_url:
  html: https://thru.org/docs/sdks/rust-program-sdk/error-handling-and-return-codes/
  md: https://thru.org/docs/sdks/rust-program-sdk/error-handling-and-return-codes.md
---

# Error Handling and Return Codes

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.

## Success and revert

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

| Return | Effect |
| - | - |
| `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:

```rust
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.

## Revert macros

```rust
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`.

## Panics

`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.

## `SyscallCode`

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

| Variant | Value |
| - | - |
| `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:

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

## `AccountError`

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

| Variant | Meaning |
| - | - |
| `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`. |

## `MemoryError`

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

## Reserved revert codes

| Code | Source |
| - | - |
| `555` | Rust panic handler. |
| `4444` | Default code for `assert_or_revert!` and `assert_eq_or_revert!`. |
| `0xBAD0A170` | `InvokeAuth` magic mismatch. |
| `0xBAD0A171` | Delegated account is not owned by the calling program. |
| `0xBAD0A173` | `InvokeAuth` account index out of range. |
| `0xBAD0A174` | Authorization check found a delegation whose authorizing program does not own the account. |

Avoid reusing these for your own errors.

## Related pages

- [Syscalls](https://thru.org/docs/sdks/rust-program-sdk/syscalls.md)
- [Cross-Program Invocation](https://thru.org/docs/sdks/rust-program-sdk/cross-program-invocation.md)
- [Runtime errors](https://thru.org/docs/spec/runtime/errors.md)
