Skip to main content

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:

What happens when a canister traps?

If a canister traps during a message execution, then 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

Special care should be taken when writing a pre_upgrade hook for your canister which will run during an upgrade. If the pre_upgrade hook traps, then the canister upgrade would fail. This is extremely important as it can mean your canister can be permanently bricked if such a trap occurs in the pre_upgrade hook as this will always run against the current canister's Wasm module. 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 -- again a trap might be the best option to ensure that there's no further damage done and the broken invariant can be investigated and fixed.

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. Unfortunately, the system does not capture a backtrace at the moment, so you won't see more information. 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.