Error handling

StdError provides useful variants related to the CosmWasm smart contract development. What if you would like to emit errors related to your business logic?

Define custom error

We start by adding a new dependency thiserror to our Cargo.toml.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
cosmwasm-std = { version = "2.0.4", features = ["staking"] }
sylvia = "1.1.0"
schemars = "0.8.12"
cosmwasm-schema = "2.0.4"
serde = "1.0.180"
cw-storage-plus = "2.0.0"
thiserror = "1.0.44"

[dev-dependencies]
sylvia = { version = "1.1.0", features = ["mt"] }
cw-multi-test = { version = "2.1.0", features = ["staking"] }

It provides an easy-to-use derive macro to set up our errors.

Let's create new file src/error.rs.

use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Cannot decrement count. Already at zero.")]
    CannotDecrementCount,
}

We annotate the ContractError enum with Error macro as well as Debug and PartialEq. Variants need to be prefixed with #[error(..)] attribute. First one will be called Std and will implement From trait on our error. This way we can both return standard CosmWasm errors and our own defined ones. For our business logic we will provide the CannotDecrementCount variant. String inside of error(..) attribute will provide Display value for readability.

Now let's add the error module to our project. In src/lib.rs:

pub mod contract;
pub mod error;
#[cfg(test)]
pub mod multitest;
pub mod responses;

Use custom error

Our error is defined. Now let's add a new ExecMsg variant.

use cosmwasm_std::{Response, StdResult};
use cw_storage_plus::Item;
use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx};
use sylvia::{contract, entry_points};

use crate::error::ContractError;
use crate::responses::CountResponse;

pub struct CounterContract {
    pub(crate) count: Item<u32>,
}

#[entry_points]
#[contract]
impl CounterContract {
    pub const fn new() -> Self {
        Self {
            count: Item::new("count"),
        }
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, ctx: InstantiateCtx, count: u32) -> StdResult<Response> {
        self.count.save(ctx.deps.storage, &count)?;
        Ok(Response::default())
    }

    #[sv::msg(query)]
    pub fn count(&self, ctx: QueryCtx) -> StdResult<CountResponse> {
        let count = self.count.load(ctx.deps.storage)?;
        Ok(CountResponse { count })
    }

    #[sv::msg(exec)]
    pub fn increment_count(&self, ctx: ExecCtx) -> StdResult<Response> {
        self.count
            .update(ctx.deps.storage, |count| -> StdResult<u32> {
                Ok(count + 1)
            })?;
        Ok(Response::default())
    }

    #[sv::msg(exec)]
    pub fn decrement_count(&self, ctx: ExecCtx) -> Result<Response, ContractError> {
        let count = self.count.load(ctx.deps.storage)?;
        if count == 0 {
            return Err(ContractError::CannotDecrementCount);
        }
        self.count.save(ctx.deps.storage, &(count - 1))?;
        Ok(Response::default())
    }
}

A little to explain here. We load the count, and check if it's equal to zero. If yes, then we return our newly defined error variant. If not, then we decrement its value. However, this won't work. If you would try to build this you will receive:

error[E0277]: the trait bound `cosmwasm_std::StdError: From<ContractError>` is not satisfied

It is because sylvia by default generates dispatch returning Result<_, StdError>. To inform sylvia that it should be using a new type we add #[sv::error(ContractError)] attribute to the contract macro call.

#[entry_points]
#[contract]
#[sv::error(ContractError)]
impl CounterContract {
...
}

Now our contract should compile, and we are ready to test it.

Testing

Let's create a new test expecting the proper error to be returned. In src/multitest.rs:

use sylvia::cw_multi_test::IntoAddr;
use sylvia::multitest::App;

use crate::contract::sv::mt::{CodeId, CounterContractProxy};
use crate::error::ContractError;

#[test]
fn instantiate() {
    let app = App::default();
    let code_id = CodeId::store_code(&app);

    let owner = "owner".into_addr();

    let contract = code_id.instantiate(42).call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 42);

    contract.increment_count().call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 43);
}

#[test]
fn decrement_below_zero() {
    let app = App::default();
    let code_id = CodeId::store_code(&app);

    let owner = "owner".into_addr();

    let contract = code_id.instantiate(1).call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 1);

    contract.decrement_count().call(&owner).unwrap();

    let count = contract.count().unwrap().count;
    assert_eq!(count, 0);

    let err = contract.decrement_count().call(&owner).unwrap_err();
    assert_eq!(err, ContractError::CannotDecrementCount);
}

We instantiate our contract with count equal to 1. First decrement_count should pass as it is above 0. Then on the second decrement_count call, we will unwrap_err and check if it matches our newly defined error variant.

Next step

We introduced proper error handling to our contract. Now we will learn about interfaces. Sylvia feature allowing to split our contract into semantic parts.