Errors
The errors example demonstrates how to define and generate errors in a contract that invokers of the contract can understand and handle. This example is an extension of the storing data example.
Run the Example
First go through the Setup process to get your development environment
configured, then clone the v20.0.0-rc2
tag of soroban-examples
repository:
git clone -b v20.0.0-rc2 https://github.com/stellar/soroban-examples
Or, skip the development environment setup and open this example in Gitpod.
To run the tests for the example, navigate to the errors
directory, and use cargo test
.
cd errors
cargo test
You should see output that begins like this:
running 2 tests
count: U32(0)
count: U32(1)
count: U32(2)
count: U32(3)
count: U32(4)
count: U32(5)
Status(ContractError(1))
contract call invocation resulted in error Status(ContractError(1))
test test::test ... ok
thread 'test::test_panic' panicked at 'called `Result::unwrap()` on an `Err` value: HostError
Value: Status(ContractError(1))
Debug events (newest first):
0: "Status(ContractError(1))"
1: "count: U32(5)"
2: "count: U32(4)"
3: "count: U32(3)"
...
test test::test_panic - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.33s
Code
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
LimitReached = 1,
}
const COUNTER: Symbol = symbol_short!("COUNTER");
const MAX: u32 = 5;
#[contract]
pub struct IncrementContract;
#[contractimpl]
impl IncrementContract {
/// Increment increments an internal counter, and returns the value. Errors
/// if the value is attempted to be incremented past 5.
pub fn increment(env: Env) -> Result<u32, Error> {
// Get the current count.
let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0.
log!(&env, "count: {}", count);
// Increment the count.
count += 1;
// Check if the count exceeds the max.
if count <= MAX {
// Save the count.
env.storage().instance().set(&COUNTER, &count);
// Return the count to the caller.
Ok(count)
} else {
// Return an error if the max is exceeded.
Err(Error::LimitReached)
}
}
}
Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0-rc2/errors
How it Works
Open the errors/src/lib.rs
file to follow along.
Defining an Error
Contract errors are Rust u32 enums where every variant of the enum is assigned
an integer. The #[contracterror]
attribute is used to set the error up so it
can be used in the return value of contract functions.
The enum has some constraints:
- It must have the
#[repr(u32)]
attribute. - It must have the
#[derive(Copy)]
attribute. - Every variant must have an explicit integer value assigned.
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
LimitReached = 1,
}
Contract errors cannot be stored as contract data, and therefore cannot be used as types on fields of contract types.
If an error is returned from a function anything the function has done is rolled back. If ledger entries have been altered, or contract data stored, all those changes are reverted and will not be persisted.
Returning an Error
Errors can be returned from contract functions by returning Result<_, E>
.
The increment function returns a Result<u32, Error>
, which means it returns
Ok(u32)
in the successful case, and Err(Error)
in the error case.
pub fn increment(env: Env) -> Result<u32, Error> {
// ...
if count <= MAX {
// ...
Ok(count)
} else {
// ...
Err(Error::LimitReached)
}
}
Panicking with an Error
Errors can also be panicked instead of being returned from the function.
The increment function could also be written as follows with a u32
return
value. The error can be passed to the environment using the panic_with_error!
macro.
pub fn increment(env: Env) -> u32 {
// ...
if count <= MAX {
// ...
count
} else {
// ...
panic_with_error!(&env, Error::LimitReached)
}
}
Functions that do not return a Result<_, E>
type do not include in their
specification what the possible error values are. This makes it more difficult
for other contracts and clients to integrate with the contract. However, this
might be ideal if the errors are diagnostic and debugging, and not intended to
be handled.
Tests
Open the errors/src/test.rs
file to follow along.
#[test]
fn test() {
let env = Env::default();
let contract_id = env.register_contract(None, IncrementContract);
let client = IncrementContractClient::new(&env, &contract_id);
assert_eq!(client.try_increment(), Ok(Ok(1)));
assert_eq!(client.try_increment(), Ok(Ok(2)));
assert_eq!(client.try_increment(), Ok(Ok(3)));
assert_eq!(client.try_increment(), Ok(Ok(4)));
assert_eq!(client.try_increment(), Ok(Ok(5)));
assert_eq!(client.try_increment(), Err(Ok(Error::LimitReached)));
std::println!("{}", env.logger().all().join("\n"));
}
#[test]
#[should_panic(expected = "Status(ContractError(1))")]
#E3256B
fn test_panic() {
let env = Env::default();
let contract_id = env.register_contract(None, IncrementContract);
let client = IncrementContractClient::new(&env, &contract_id);
assert_eq!(client.increment(), 1);
assert_eq!(client.increment(), 2);
assert_eq!(client.increment(), 3);
assert_eq!(client.increment(), 4);
assert_eq!(client.increment(), 5);
client.increment();
}
In any test the first thing that is always required is an Env
, which is the
Soroban environment that the contract will run in.
let env = Env::default();
The contract is registered with the environment using the contract type.
let contract_id = env.register_contract(None, IncrementContract);
All public functions within an impl
block that is annotated with the
#[contractimpl]
attribute have a corresponding function generated in a
generated client type. The client type will be named the same as the contract
type with Client
appended. For example, in our contract the contract type is
IncrementContract
, and the client is named IncrementContractClient
.
let client = IncrementContractClient::new(&env, &contract_id);
Two functions are generated for every contract function, one that returns a
Result<>
, and the other that does not handle errors and panicks if an
error occurs.
try_increment
In the first test the try_increment
function is called and returns
Result<Result<u32, _>, Result<Error, Status>>
.
assert_eq!(client.try_increment(), Ok(Ok(5)));
assert_eq!(client.try_increment(), Err(Ok(Error::LimitReached)));
If the function call is successful,
Ok(Ok(u32))
is returned.If the function call is successful but returns a value that is not a
u32
,Ok(Err(_))
is returned.If the function call is unsuccessful,
Err(Ok(Error))
is returned.If the function call is unsuccessful but returns an error code not in the
Error
enum, or returns a system error code,Err(Err(Status))
is returned and theStatus
can be inspected.
increment
In the second test the increment
function is called and returns u32
. When
the last call is made the function panicks.
assert_eq!(client.increment(), 5);
client.increment();
If the function call is successful,
u32
is returned.If the function call is successful but returns a value that is not a
u32
, a panic occurs.If the function call is unsuccessful, a panic occurs.
Build the Contract
To build the contract, use the soroban contract build
command.
soroban contract build
A .wasm
file should be outputted in the target
directory:
target/wasm32-unknown-unknown/release/soroban_errors_contract.wasm
Run the Contract
If you have soroban-cli
installed, you can invoke contract functions in the
Wasm using it.
soroban contract invoke \
--wasm target/wasm32-unknown-unknown/release/soroban_errors_contract.wasm \
--id 1 \
-- \
increment
Run the command a few times and on the 6th invocation the following output should occur.
❯ soroban contract invoke \
--wasm target/wasm32-unknown-unknown/release/soroban_errors_contract.wasm \
--id 1 \
-- \
increment
error: HostError
Value: Status(ContractError(1))
...
Use the soroban
to inspect what the counter is.
soroban contract read --id 1 --key COUNTER