Trapping
Overview
Trapping, in the context of WebAssembly code, refers to an interruption of code execution that returns an error. Trapping is defined by the WebAssembly documentation as:
“Under some conditions, certain instructions may produce a trap, which immediately aborts execution. Traps cannot be handled by WebAssembly code, but are reported to the outside environment, where they typically can be caught.”
On ICP, canister code is compiled into WebAssembly before it is executed. This means that canisters can trap. Some example cases when canisters might trap include:
- Canister code panics (e.g. in Rust).
- Dividing by zero.
- Calling a System API in a context that is not permitted by ICP (see a list of APIs and which contexts they are available in).
- Accessing heap data that is out of bounds.
What happens when a canister traps?
If a canister traps during a message execution, two things happen:
- Message execution immediately ends and an error is returned.
- Any state changes made by the canister during this message execution will be rolled back.
Traps during upgrades
Using pre_ and post_upgrade hooks is discouraged. It is error-prone and can render a canister unusable. Per best practices, using these methods should be avoided if possible.
Special care should be taken when writing a pre_upgrade
hook for your canister which will run during an upgrade against the current canister's Wasm module. If the pre_upgrade hook traps, then the canister upgrade will fail. This is extremely important as it can mean your canister can be permanently unrecoverable.
In order to avoid unexpected surprises, it is strongly recommended that your canisters use stable memory directly, which removes the need for having a pre_upgrade
hook in the first place.
Traps during inter-canister calls
Trapping is an important concern when using inter-canister calls. Since the state is rolled back only for the current message execution, this means that if there's a trap in a callback handler execution, the state only rolls back to the point before this message execution. In particular, any state changes made by the canister before making the inter-canister call and calling await
to wait for the response will not be rolled back (as this was a separate message execution that happened before the callback handler was invoked).
This is quite important as it might affect how one can implement canisters safely. Generally speaking, traps should be avoided in callback handler invocations. If this is not possible, then it's recommended to use the cleanup system API call to ensure that any cleanup action is performed (e.g. releasing a lock that was acquired before making an inter-canister call).
When to use traps explicitly
Traps can also be useful in certain situations. If you encounter an unrecoverable error while processing a message, it might be better to trap and roll back any changes made in that message execution to avoid leaving your canister in a weird state. Another case would be if an invariant that should generally hold is violated.
Troubleshooting why your canister trapped
Some of the common root causes of canister traps include:
“Heap out of bounds”: Usually means that your canister is attempting to access heap memory that has not been allocated yet. This typically points to a programming error. Look for places where memory is allocated (e.g. creating vectors) and whether you try to access such memory before allocating it.
“Stable memory out of bounds”: Similar to “heap out of bounds” but for stable memory.
“Integer division by zero”: The canister attempted to divide by zero. Inspect the canister's code for any division operations.
“Unreachable”: Typically produced when the canister panic's, e.g. for Rust canisters. It's recommended to use the panic hook so that panic's are converted to an explicit call to ic0.trap which will append an error message that will be more useful for debugging.
Errors related to canister trapping
Common errors related to canister trapping include: