cosmwasm: How to distinguish errors in smart contracts?

As I’ve seen so far, developers define their errors using StdErr:generic_err("this is error message"). In testing, I need to check if a handler throws the expected error. Is pattern matching internal message string the only way?

Update after the discussion, I pulled an approach we agreed upon into two separate issues:

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 26 (26 by maintainers)

Most upvoted comments

With this we could have:

pub fn handle<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    msg: HandleMsg,
) -> Result<HandleResponse, MyCustomError> { }

And then do something like:

pub enum MyCustomError {
    Std(StdError),
    // this is whatever we want
    Special{ count: i32 },
}

impl Into<StdError> for MyCustomError {
  fn into(self) -> StdError {
    match self {
      Std(err) => err,
      Special{count} => StdError::generic_err(format!("Special #{}", count)),
  }
}

And to make this nicer when using, eg. .may_load(..)? which returns StdError:

impl From<StdError> for MyCustomError {
  fn from(orig: StdError) -> Self {
    MyCustomError::Std(orig)
  }
}

It would buy us more clarity in the unit tests (even using handle, init high-level interfaces), but would all compress down to strings when running in a vm.

and simplify the public interface (vm, go-cosmwasm) at the same time. I guess this goes along with my emphasis on unit tests over integration tests recently

This also goes along with respecting that unit tests and integration tests are fundamentally different things. It will make supporting other languages much easier since they only need to implement creating a compatible serialization of Result<HandleResponse, String> instead of Result<HandleResponse<U>, StdError>.

You can easily test your approach without affecting the framework:

pub fn handle<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    msg: HandleMsg,
)  -> StdResult<HandleResponse> {
    Ok(handle_impl(deps, env, msg)?)
}

pub fn handle_impl<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    msg: HandleMsg,
) -> Result<HandleResponse, MyCustomError> { }

and then test handle_impl in unit tests.

Looks good. I can try these patterns in the unit tests for one of the contracts I worked on recently, like subkeys or atomic swaps.

@maurolacy this will break wasm builds as the various cosmwasm-std::entry_points code requires StdError return. Best to try to update one of the contracts in cosmwasm-std, so we can try it and update the other cosmwasm-std deps in one PR. They are all quite simple contracts. queue is probably the simplest one, and hackatom is where we put most of the generic test cases

A best practice test for this looks like

    let res: InitResult = init(&mut deps, env, msg);
    match res.unwrap_err() {
        StdError::GenericErr { msg, .. } => {
            assert_eq!(msg, "You can only use this contract for migrations")
        }
        err => panic!("Unexpected error: {:?}", err),
    }

The create assert_matches can potentially add syntactic sugar to it. I never used it but know it is used by the NEAR team. Probably worth trying it internally at some point and sharing experience to evaluate if it is worth an additional dependency.

If you consider it’s worth it, you can always add specific enums for identifying your errors.

This is true for Rust in general. However, StdError and StdResult used in the contract-VM interface do no support that.