Errors

Errors can be used to handle validation and other conditions that may occur during the execution of a smart contract. If an error is thrown during the execution of a smart contract call, the execution is stopped and any changes made during the transaction are reverted.

To throw an error, use the assert or panic functions:

  • assert is used to validate conditions. If the check fails, an error is thrown along with a specified value, often a message. It's similar to the require statement in Solidity.

  • panic immediately halt the execution with the given error value. It should be used when the condition to check is complex and for internal errors. It's similar to the revert statement in Solidity. (Use panic_with_felt252 to be able to directly pass a felt252 as the error value)

The assert_eq!, assert_ne!, assert_lt!, assert_le!, assert_gt! and assert_ge! macros can be used as an assert shorthand to compare two values, but only in tests. In contract, you should only use the assert function.

Here's a simple example that demonstrates the use of these functions:

#[starknet::interface]
pub trait IErrorsExample<TContractState> {
    fn test_assert(self: @TContractState, i: u256);
    fn test_panic(self: @TContractState, i: u256);
}

#[starknet::contract]
pub mod ErrorsExample {
    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl ErrorsExample of super::IErrorsExample<ContractState> {
        fn test_assert(self: @ContractState, i: u256) {
            // Assert used to validate a condition
            // and abort execution if the condition is not met
            assert(i > 0, 'i must be greater than 0');
        }

        fn test_panic(self: @ContractState, i: u256) {
            if (i == 0) {
                // Panic used to abort execution directly
                core::panic_with_felt252('i must not be 0');
            }
        }
    }
}

#[cfg(test)]
mod test {
    use super::{ErrorsExample, IErrorsExampleDispatcher, IErrorsExampleDispatcherTrait};
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    fn deploy() -> IErrorsExampleDispatcher {
        let (contract_address, _) = deploy_syscall(
            ErrorsExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        IErrorsExampleDispatcher { contract_address }
    }

    #[test]
    #[should_panic(expected: ('i must not be 0', 'ENTRYPOINT_FAILED'))]
    fn should_panic() {
        let contract = deploy();
        contract.test_panic(0);
    }

    #[test]
    #[should_panic(expected: ('i must be greater than 0', 'ENTRYPOINT_FAILED'))]
    fn should_assert() {
        let contract = deploy();
        contract.test_assert(0);
    }
}

Custom errors

You can make error handling easier by defining your error codes in a specific module.

#[starknet::interface]
pub trait ICustomErrorsExample<TContractState> {
    fn test_assert(self: @TContractState, i: u256);
    fn test_panic(self: @TContractState, i: u256);
}

pub mod Errors {
    pub const NOT_POSITIVE: felt252 = 'must be greater than 0';
    pub const NOT_NULL: felt252 = 'must not be null';
}

#[starknet::contract]
pub mod CustomErrorsExample {
    use super::Errors;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl CustomErrorsExample of super::ICustomErrorsExample<ContractState> {
        fn test_assert(self: @ContractState, i: u256) {
            assert(i > 0, Errors::NOT_POSITIVE);
        }

        fn test_panic(self: @ContractState, i: u256) {
            if (i == 0) {
                core::panic_with_felt252(Errors::NOT_NULL);
            }
        }
    }
}

#[cfg(test)]
mod test {
    use super::{
        CustomErrorsExample, ICustomErrorsExampleDispatcher, ICustomErrorsExampleDispatcherTrait
    };
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    fn deploy() -> ICustomErrorsExampleDispatcher {
        let (contract_address, _) = deploy_syscall(
            CustomErrorsExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        ICustomErrorsExampleDispatcher { contract_address }
    }

    #[test]
    #[should_panic(expected: ('must not be null', 'ENTRYPOINT_FAILED'))]
    fn should_panic() {
        let contract = deploy();
        contract.test_panic(0);
    }

    #[test]
    #[should_panic(expected: ('must be greater than 0', 'ENTRYPOINT_FAILED'))]
    fn should_assert() {
        let contract = deploy();
        contract.test_assert(0);
    }
}

Vault example

Here's another example that demonstrates the use of errors in a more complex contract:

#[starknet::interface]
pub trait IVaultErrorsExample<TContractState> {
    fn deposit(ref self: TContractState, amount: u256);
    fn withdraw(ref self: TContractState, amount: u256);
}

pub mod VaultErrors {
    pub const INSUFFICIENT_BALANCE: felt252 = 'insufficient_balance';
// you can define more errors here
}

#[starknet::contract]
pub mod VaultErrorsExample {
    use super::VaultErrors;

    #[storage]
    struct Storage {
        balance: u256,
    }

    #[abi(embed_v0)]
    impl VaultErrorsExample of super::IVaultErrorsExample<ContractState> {
        fn deposit(ref self: ContractState, amount: u256) {
            let mut balance = self.balance.read();
            balance = balance + amount;
            self.balance.write(balance);
        }

        fn withdraw(ref self: ContractState, amount: u256) {
            let mut balance = self.balance.read();

            assert(balance >= amount, VaultErrors::INSUFFICIENT_BALANCE);

            // Or using panic:
            if (balance < amount) {
                core::panic_with_felt252(VaultErrors::INSUFFICIENT_BALANCE);
            }

            let balance = balance - amount;

            self.balance.write(balance);
        }
    }
}

#[cfg(test)]
mod test {
    use super::{
        VaultErrorsExample, IVaultErrorsExampleDispatcher, IVaultErrorsExampleDispatcherTrait
    };
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    fn deploy() -> IVaultErrorsExampleDispatcher {
        let (contract_address, _) = deploy_syscall(
            VaultErrorsExample::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        IVaultErrorsExampleDispatcher { contract_address }
    }

    #[test]
    fn should_deposit_and_withdraw() {
        let mut contract = deploy();
        contract.deposit(10);
        contract.withdraw(5);
    }

    #[test]
    #[should_panic(expected: ('insufficient_balance', 'ENTRYPOINT_FAILED'))]
    fn should_panic_on_insufficient_balance() {
        let mut contract = deploy();
        contract.deposit(10);
        contract.withdraw(15);
    }
}
Last change: 2024-05-03, commit: dd8c9eb