Is trapping better than returning an error?

When designing the canister interface we can choose between trapping, rejecting and returning an ok/error variant. Suppose we want to abort a call without any state changes so we actually have the choice between all three. Is trapping more friendly to the resources of the IC because there is no state update? Rejecting and returning an error commits to a state update even though the state may have changed only marginally (in a non-relevant way) or not at all. In this case, how large is the difference in terms of resource consumption between trapping and the other two options?

And is there a difference in resource consumption in message routing for handling the response?

As you say, if you return an error, any changes you made to the canister state (even temporary heap allocations, e.g. to construct a response) must be persisted by the subnet. The subnet/replica has no idea whether some change you made to the heap is a significant state change that may affect later calls or not. In such a case, depending on how much heap was mutated, it’s more efficient to trap and have all those mutations discarded.

If OTOH the handler doesn’t allocate any heap (with everything happening on the stack), then I believe that there should be no material difference between trapping and returning an error.

Message Routing and/or the ingress handler make no difference between canister responses (which is what gets produced if you return an error) and reject responses (produced by the Execution Environment when your code traps).

1 Like

And for query it doesn’t matter, right? I mean if I call a query function in update mode (replicated). Then trapping or returning an error does not make any difference for resource consumption.

I’m not actually sure, but it does make sense for query calls to never mutate the state of the canister, regardless of how they are executed (as a query or as an update).

Yes, query always discards any changes made to memory after it is complete. In update if you want to make sure that nothing has change use trap

I think it will make sense to adopt Result<,> type:

  • signaling that the call intent failed, but the transaction was processed.

For composability, it’s easier to reason about errors rather than panics.

That is an argument but does it always apply? I would like to understand the details, when does it apply, and what trade-offs are being made.

For example, in cases where the error is the caller’s fault, due to supplying invalid arguments. That should not happen in normal operation. The error is then primarily used only for debugging purposes during development, not during normal operation. For debugging trapping with a message seems fine. If returning a Result type is more expensive than trapping then why should the canister bear the cost when the caller supplies invalid arguments?

Does composability specifically refer to inter-canister calls (not ingress messages)?