Traps and Commit Points - throw confusion

Hey,

Actors and async data :: Internet Computer says that the points at which tentative state changes and message sends are irrevocably committed are:

  • await, return ,… but also throw
    Why is state not reverted after a throw statement? If division by zero happens or an assert fails then the state is also reverted, right? BUt why not through throw statement?
    Is it possible to catch the thrown error in the local canister? In this case state revert make no sense, but if the thrown error is not catched in the local canister then the state should be reverted, why is it not the case?

Thanks

1 Like

This is the explicit throw of Motoko, which maps to issuing a “reject” on the system level. On the system level, calls have either a reply or a reject as their response.

Don’t confuse throw (aka reject) with the concept of a trap, which happens when you divide by zero. That is not catchable within WebAssembly. Mildly confusing, it often appears that a trap causes a reject, but that is not always the case, and in some sense a trap in the canister causes a the system to response with a reject on the canister’s behalf.

1 Like

Isn’t “reject” not something that tells the caller that something went bad on the calling-side? In this case the caller knows his call failed and can assume that it didn’t lead to some state changes on the calling side. I don’t know, to me is an trap and an uncaught exception conceptually the same. For example in Java JPA, if an uncaught exceptions occurres inside a transaction then the transaction fails and the state will be reverted thats why it looks a bit weird that an uncaught exception through a throw sends a reject but don’t revert its state to last commit point.

it often appears that a trap causes a reject, but that is not always the case

In which cases can a trap not lead to a reject if it is uncatchable?

1 Like

Rejects don’t carry lots of hard guarantees, they are mostly convention. Canisters can generate them at will, and still commit their state changes. They are not like traps or exceptions.

Traps are a wasm concept and relate to a single message execution. A canister can respond to a call in any of possibly multiple callcack handlers, and if an trap occurs in one while others are still outstanding (and thus could respond), this trap does not generate a reject.

1 Like

Follow-up question: when an unexpected situation happens (like looking up an unknown ID in a Trie), should the canister return a null in an Option<T> or is it ever OK for the canister method to trap instead? What is the use case of a trap?

I’m guessing it returns something different to the client.

It depends :-).

Traps are good for unexpected error conditions that cannot be recovered from, and where you simply want to ignore the current message. Option is good when the caller of your code likely wants to handle that case programmatically.

Hm interesting. When you mean ignore the current message, I’m assuming that applies more to updates, since any state change will be reverted? Whereas I guess if you return an Option, the state change is kept.

Exactly! Maybe this summarizes the intention of a canister well:

  • Trap (via assert false, or soon Debug.trap): “I am completely lost, better be safe and don’t record any state changes”. Same as division by 0 for example.
  • Rejecting (via throw): “The call cannot be performed, likely because the caller did something stupid, but I am still pretty sure that my state is in good order, so it can be committed”. Also “Call failed, but I maybe want to record the fact that this caller tried something”.
  • Replying, with null or #error "messages": “The caller did everything right, I am also still healthy, and and as part of the expected way of things I am returning some kind of negative result (e.g. key not present in a key value store)”.
  • Replying, with data: “All well, here you go, have a good day”.
5 Likes

Great response. I had no idea throws committed state but traps reverted state (even though both result in the canister responding with a reject, i.e. an Error in JS land if using agent-js).

It’s also interesting that a public method can throw a Motoko Error without having to specify that possibility in the method signature. This is different from returning a Result<Ok, Err>, where the error type is explicit in the type.

For example, the compiler thinks this is totally fine:

public func bar(): async Nat {
throw Error.reject(“bar got rejected!”);
};

1 Like

If the main advantage of throw over assert is the ability to commit state changes, shouldn’t the assign_role function in this code snippet modify role_requests at the top of the function instead of at the bottom? So that the request is logged even if the function throws?