Skip to main content
Proper error handling makes contracts more robust, debuggable, and user-friendly. Soroban provides the #[contracterror] macro to define custom error types that can be returned from contract functions.

Why Custom Errors?

Custom errors provide:
  • Clear error codes: Specific numeric codes for each error condition
  • Type safety: Compile-time checking of error handling
  • Better debugging: Named errors are easier to understand than generic failures
  • Documentation: Error types serve as documentation of failure modes

Defining Error Types

Use the #[contracterror] macro to define error enums:
use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
    NotFound = 1,
    Unauthorized = 2,
    InsufficientBalance = 3,
    InvalidAmount = 4,
}
Error codes must be unique u32 values. Error code 0 is reserved and cannot be used.

Returning Errors

Use Result<T, Error> as the return type for functions that can fail:
#[contractimpl]
impl Token {
    pub fn transfer(
        env: Env,
        from: Address,
        to: Address,
        amount: i128,
    ) -> Result<(), Error> {
        from.require_auth();

        if amount <= 0 {
            return Err(Error::InvalidAmount);
        }

        let from_balance = Self::get_balance(&env, &from);
        if from_balance < amount {
            return Err(Error::InsufficientBalance);
        }

        // Perform transfer
        Self::set_balance(&env, &from, from_balance - amount);
        let to_balance = Self::get_balance(&env, &to);
        Self::set_balance(&env, &to, to_balance + amount);

        Ok(())
    }
}

Complete Example

Here’s a complete contract with comprehensive error handling:
#![no_std]
use soroban_sdk::{
    contract, contracterror, contractimpl, contracttype,
    panic_with_error, symbol_short, Address, Env, Symbol
};

#[contract]
pub struct Contract;

#[contracttype]
#[derive(PartialEq)]
pub enum Flag {
    A = 0,
    B = 1,
    C = 2,
    D = 3,
    E = 4,
}

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
    AnError = 1,
    NotAuthorized = 2,
    InvalidInput = 3,
}

#[contractimpl]
impl Contract {
    pub fn hello(env: Env, flag: Flag) -> Result<Symbol, Error> {
        env.storage()
            .persistent()
            .set(&symbol_short!("persisted"), &true);

        match flag {
            Flag::A => Ok(symbol_short!("hello")),
            Flag::B => Err(Error::AnError),
            Flag::C => {
                // Immediately panic with error
                panic_with_error!(&env, Error::AnError)
            },
            Flag::D => {
                // Regular panic (not a contract error)
                panic!("an error")
            },
            Flag::E => {
                // Panic with generic error code
                panic_with_error!(&env, soroban_sdk::Error::from_contract_error(9))
            },
        }
    }
}

Error Handling Strategies

Returning Errors

Return errors for recoverable failures:
pub fn withdraw(env: Env, amount: i128) -> Result<(), Error> {
    let balance = get_balance(&env);
    
    if amount > balance {
        return Err(Error::InsufficientBalance);
    }
    
    set_balance(&env, balance - amount);
    Ok(())
}

Panic with Error

Use panic_with_error! to immediately halt execution:
use soroban_sdk::panic_with_error;

pub fn admin_only(env: Env, caller: Address) {
    let admin = get_admin(&env);
    
    if caller != admin {
        panic_with_error!(&env, Error::Unauthorized);
    }
    
    // Continue with admin operations
}
When a function panics (either with panic! or panic_with_error!), all storage changes are rolled back.

Propagating Errors

Use the ? operator to propagate errors:
pub fn complex_operation(env: Env) -> Result<i128, Error> {
    // Error automatically propagates if validate_input fails
    validate_input(&env)?;
    
    // Error automatically propagates if process_data fails
    let result = process_data(&env)?;
    
    Ok(result)
}

fn validate_input(env: &Env) -> Result<(), Error> {
    // validation logic
    Ok(())
}

fn process_data(env: &Env) -> Result<i128, Error> {
    // processing logic
    Ok(42)
}

Testing Errors

Use try_ prefixed methods to test error cases:
#[test]
fn test_success() {
    let env = Env::default();
    let contract_id = env.register(Contract, ());
    let client = ContractClient::new(&env, &contract_id);

    let result = client.try_hello(&Flag::A);
    assert_eq!(result, Ok(Ok(symbol_short!("hello"))));
}

Testing Storage Rollback

Verify that storage changes are rolled back on error:
#[test]
fn test_rollback_on_error() {
    let env = Env::default();
    let contract_id = env.register(Contract, ());
    let client = ContractClient::new(&env, &contract_id);

    // This will fail and rollback storage changes
    let _ = client.try_hello(&Flag::B);
    
    // Storage should not be persisted
    assert!(!client.persisted());
}

#[test]
fn test_persist_on_success() {
    let env = Env::default();
    let contract_id = env.register(Contract, ());
    let client = ContractClient::new(&env, &contract_id);

    // This succeeds
    client.hello(&Flag::A);
    
    // Storage should be persisted
    assert!(client.persisted());
}

Error Type Conversions

Contract errors can be converted to and from other error types:
#[test]
fn test_error_conversions() {
    use soroban_sdk::{Error as SdkError, InvokeError};

    // Contract Error -> InvokeError
    let invoke_err: InvokeError = Error::AnError.into();
    assert_eq!(invoke_err, InvokeError::Contract(1));

    // InvokeError -> Contract Error
    let result: Result<Error, InvokeError> = 
        InvokeError::Contract(1).try_into();
    assert_eq!(result, Ok(Error::AnError));

    // Contract Error -> SDK Error
    let sdk_err: SdkError = Error::AnError.into();
    assert_eq!(sdk_err, SdkError::from_contract_error(1));

    // SDK Error -> Contract Error
    let result: Result<Error, SdkError> = 
        SdkError::from_contract_error(1).try_into();
    assert_eq!(result, Ok(Error::AnError));
}

InvokeError

When calling other contracts, errors are returned as InvokeError:
use soroban_sdk::InvokeError;

pub enum InvokeError {
    // Contract panicked or host function failed
    Abort,
    // Contract returned a contract error
    Contract(u32),
}
Handle errors from other contracts:
pub fn call_other_contract(env: Env, contract: Address) -> Result<i128, Error> {
    let client = OtherContractClient::new(&env, &contract);
    
    match client.try_some_function() {
        Ok(value) => Ok(value),
        Err(Ok(other_error)) => {
            // Handle specific contract error
            Err(Error::ExternalContractFailed)
        },
        Err(Err(InvokeError::Abort)) => {
            // Handle abort/panic
            Err(Error::ExternalContractPanicked)
        },
        Err(Err(InvokeError::Contract(code))) => {
            // Handle unexpected contract error code
            Err(Error::UnexpectedError)
        }
    }
}

Error Best Practices

pub enum Error {
    InsufficientBalance = 1,  // Good: clear meaning
    Err1 = 1,                  // Bad: unclear
}
Error code 0 is reserved and should not be used.
pub enum Error {
    FirstError = 1,  // Good: starts at 1
    SecondError = 2,
}
pub enum Error {
    /// Returned when the caller is not authorized to perform the action
    Unauthorized = 1,
    /// Returned when an account has insufficient balance for the operation
    InsufficientBalance = 2,
}
Write tests that exercise every error condition:
#[test]
fn test_all_errors() {
    test_unauthorized();
    test_insufficient_balance();
    test_invalid_amount();
    // ... test each error variant
}

Common Error Patterns

Authorization Errors

pub fn protected_action(env: Env, caller: Address) -> Result<(), Error> {
    let admin = get_admin(&env);
    
    if caller != admin {
        return Err(Error::Unauthorized);
    }
    
    Ok(())
}

Validation Errors

pub fn set_price(env: Env, price: i128) -> Result<(), Error> {
    if price <= 0 {
        return Err(Error::InvalidPrice);
    }
    
    if price > MAX_PRICE {
        return Err(Error::PriceTooHigh);
    }
    
    env.storage().persistent().set(&DataKey::Price, &price);
    Ok(())
}

State Errors

pub fn initialize(env: Env, admin: Address) -> Result<(), Error> {
    if env.storage().instance().has(&DataKey::Initialized) {
        return Err(Error::AlreadyInitialized);
    }
    
    env.storage().instance().set(&DataKey::Admin, &admin);
    env.storage().instance().set(&DataKey::Initialized, &true);
    Ok(())
}

Error vs Panic

  • The error is expected/recoverable
  • Caller should handle the error
  • You want to provide specific error information
  • Multiple error conditions exist

Next Steps

Testing

Learn to test error handling

Custom Types

Define custom types for your errors