Skip to main content
Version: next

Errors

Odra comes with tools that allow you to throw, handle and test for errors in execution. Take a look at the following example of a simple owned contract:

examples/src/features/handling_errors.rs
use odra::prelude::*;

#[odra::module(errors = Error)]
pub struct OwnedContract {
name: Var<String>,
owner: Var<Address>
}

#[odra::odra_error]
pub enum Error {
OwnerNotSet = 1,
NotAnOwner = 2
}

#[odra::module]
impl OwnedContract {
pub fn init(&mut self, name: String) {
self.name.set(name);
self.owner.set(self.env().caller())
}

pub fn name(&self) -> String {
self.name.get_or_default()
}

pub fn owner(&self) -> Address {
self.owner.get_or_revert_with(Error::OwnerNotSet)
}

pub fn change_name(&mut self, name: String) {
let caller = self.env().caller();
if caller != self.owner() {
self.env().revert(Error::NotAnOwner)
}

self.name.set(name);
}
}

Firstly, we are using the #[odra::odra_error] attribute to define our own set of Errors that our contract will throw. Then, you can use those errors in your code - for example, instead of forcefully unwrapping Options, you can use get_or_revert_with and pass an error as an argument:

examples/src/features/handling_errors.rs
self.owner.get_or_revert_with(Error::OwnerNotSet)
note

Var also exposes the underlying unwrap_or_revert_with, but that one comes from the UnwrapOrRevert trait and takes two arguments - the reverting module and the error: self.owner.get().unwrap_or_revert_with(self, Error::OwnerNotSet). On a Var, prefer the single-argument get_or_revert_with shown above.

You can also throw the error directly, by using revert:

examples/src/features/handling_errors.rs
self.env().revert(Error::NotAnOwner)

To register errors, add the errors inner attribute to the struct's #[odra::module] attribute and pass the error type as the value. The registered errors will be present in the contract schema.

Defining an error in Odra, you must keep in mind a few rules:

  1. An error should be a field-less enum.
  2. The enum must be annotated with #[odra::odra_error].
  3. Avoid implicit discriminants.
note

In your project you can define as many error enums as you wish, but the discriminants must be unique within a contract: a failed call reports only the number, and the errors of all the modules a contract is composed of share one range of codes. Since Odra 2.11.0, cargo odra schema and cargo odra build stop when two errors of a contract use the same code, and name both. Using the same enum in several modules is fine.

Testing errors​

Okay, but how about testing it? Let's write a test that will check if the error is thrown when the caller is not an owner:

examples/src/features/handling_errors.rs
#[cfg(test)]
mod tests {
use super::{Error, OwnedContract, OwnedContractInitArgs};
use odra::{host::Deployer, prelude::*};

#[test]
fn test_owner_error() {
let test_env = odra_test::env();
let owner = test_env.get_account(0);
let not_an_owner = test_env.get_account(1);

test_env.set_caller(owner);
let init_args = OwnedContractInitArgs {
name: "OwnedContract".to_string()
};
let mut owned_contract = OwnedContract::deploy(&test_env, init_args);

test_env.set_caller(not_an_owner);
assert_eq!(
owned_contract
.try_change_name("NewName".to_string())
.unwrap_err(),
Error::NotAnOwner.into()
);
}
}

Each deployed contract is of {{ModuleName}}HostRef type and has try_{{entry_point_name}} functions that return an OdraResult. OwnedContractHostRef implements regular entrypoints: name, owner, change_name, and and safe its safe version: try_name, try_owner, try_change_name.

In our example, we are calling try_change_name and expecting an error to be thrown. For assertions, we are using a standard assert_eq! macro. As the contract call returns an OdraError, we need to convert our custom error to OdraError using Into::into().

note

On livenet the node reports only the error code; Odra names it using the contract schemas, so keep them up to date with cargo odra schema. See Livenet errors.

Error codes​

When a transaction fails on Casper, the node reports only a number, for example:

"error_message": "User error: 64653"

Odra maps every error, both yours and its own, to a single u16 code that is passed to the host as a Casper User error. Two ranges are used:

  • User errors - the discriminants you define in enums annotated with #[odra::odra_error] are reported as-is. Keep them below 64535 (MaxUserError); a code of 64536 or higher is replaced with UserErrorTooHigh (64536).
  • Internal Odra errors - ExecutionError variants raised by the framework itself. Their on-chain code is 64536 + <internal code>, so any code above 64536 is an internal Odra error.

To decode an internal error, subtract 64536 from the reported code. In the example above, 64653 - 64536 = 117, which is KeyNotFound.

note

The livenet backend does this translation for you - try_* calls return the matching ExecutionError, and user errors are resolved to their names using the contract schema. The table below is for cases when you only have the raw code, for example from a block explorer or the node RPC.

ErrorInternal codeOn-chain codeDescription
UnwrapError164537Unwrap error
UnexpectedError264538Something unexpected happened
AdditionOverflow10064636Addition overflow
SubtractionOverflow10164637Subtraction overflow
NonPayable10264638Method does not accept deposit
TransferToContract10364639Can't transfer tokens to contract
ReentrantCall10464640Reentrant call detected
CannotOverrideKeys10564641Contract already installed
UnknownConstructor10664642Unknown constructor
NativeTransferError10764643Native transfer error
IndexOutOfBounds10864644Index out of bounds
ZeroAddress10964645Tried to construct a zero address
AddressCreationFailed11064646Address creation failed
EarlyEndOfStream11164647Early end of stream - deserialization error
Formatting11264648Formatting error - deserialization error
LeftOverBytes11364649Left over bytes - deserialization error
OutOfMemory11464650Out of memory
NotRepresentable11564651Not representable
ExceededRecursionDepth11664652Exceeded recursion depth
KeyNotFound11764653Key not found
CouldNotDeserializeSignature11864654Could not deserialize signature
TypeMismatch11964655Type mismatch
CouldNotSignMessage12064656Could not sign message
EmptyDictionaryName12164657Empty dictionary name
MissingArg12264658Calling a contract with missing entrypoint arguments
MissingAddress12364659Reading the address from the storage failed
OutOfGas12464660Out of gas error
MainPurseError12564661MainPurse error
ConversionError12664662Conversion error
ContractDeploymentError12764663Couldn't deploy the contract
CannotExtractCallerInfo12864664Couldn't extract caller info
ContractNotInstalled12964665Upgrading a contract that is not installed
UpgradingWithoutPreviousVersion13064666Upgrading a contract without previous version
UpgradingNotAContract13164667Upgrading not a contract
SchemaMismatch13264668Upgrading a contract with a schema that does not match the previous version
CannotDisablePreviousVersion13364669Cannot disable a previous version of a contract
CannotUpgradeWithoutUpgrade13464670Cannot upgrade a contract without an upgrade function
FactoryModuleCall13564671Factory module function should not be called directly
CannotGetAnImmediateCaller13664672Cannot get an immediate caller
PathIndexOutOfBounds13764673Path index out of bounds
InvalidArg13864674A named argument is present but cannot be read as the expected type

Two more codes are reserved:

ErrorOn-chain codeDescription
MaxUserError64535Upper bound of the user error space
UserErrorTooHigh64536A user error with a code of 64536 or higher was thrown

What's next​

We will learn how to emit and test events using Odra.