Result and option
In Rust, Result and Option are enum types, which means they contain values within their variants:
enum Option<T> {
Some(T),
// existence
None, // non-existence
}
enum Result<T, E> {
Ok(T),
// success
Err(E), // failure
}
Result
Result is an enum type, Result<T, E>, where both T and E are generics, representing success and failure. Result types are often used in entry points and handlers:
- Ok(T) : a Result container which has succeeded, containing T
- Err(E) : a Result container which has failed, containing E
Many contract entry points are typed Result<Response, ContractError>, where Response is the Right or Success branch, while ContractError is the Left or failure case. For example, looking at the CW20-base we can see that execute is typed Result<Response, ContractError>, and the function call that matches ExecuteMsg::Transfer would be execute_transfer which shows how the result is returned:
pub fn execute_transfer(
deps: DepsMut,
_env: Env,
info: MessageInfo,
recipient: String,
amount: Uint128,
) -> Result<Response, ContractError> {
if amount == Uint128::zero() {
return Err(ContractError::InvalidZeroAmount {});
}
let rcpt_addr = deps.api.addr_validate(&recipient)?;
BALANCES.update(
deps.storage,
&info.sender,
|balance: Option<Uint128>| -> StdResult<_> {
Ok(balance.unwrap_or_default().checked_sub(amount)?)
},
)?;
BALANCES.update(
deps.storage,
&rcpt_addr,
|balance: Option<Uint128>| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
)?;
let res = Response::new()
.add_attribute("action", "transfer")
.add_attribute("from", info.sender)
.add_attribute("to", recipient)
.add_attribute("amount", amount);
Ok(res)
}
StdResult
It is also important to be mindful of StdResult, as it is frequently utilized in query handlers and the functions invoked by them.
For instance, in the nameservice contract, you can observe the StdResult, which functions similarly to Result, but lacks a specified error branch:
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::ResolveRecord { name } => query_resolver(deps, env, name),
QueryMsg::Config {} => to_binary(&config_read(deps.storage).load()?),
}
}
Let's see the implementation of query_resolver:
fn query_resolver(deps: Deps, _env: Env, name: String) -> StdResult<Binary> {
let key = name.as_bytes();
let address = match resolver_read(deps.storage).may_load(key)? {
Some(record) => Some(String::from(&record.owner)),
None => None,
};
let resp = ResolveRecordResponse { address };
to_binary(&resp)
}
It is essential to match or unwrap your container types accurately, enabling you to work with the values contained within them.
Option
In Rust, there is no concept of nil or null, unlike in most other mainstream programming languages. Instead, Rust employs the Option type, which encapsulates the notion of presence or absence within a container type.
Option<T>
is an enum type, with two variants:
Some(<wrapped-value>)
: Wraps an inner value, which can be accessed via.unwrap()
.None
: Represents the absence of a value and is often used as a default or placeholder value when a value is not yet known or cannot be determined.