I’m still a little confused in what cases does the catch block run in terms of trying an inter-canister call.
Let’s say I have code:
public func someFunc() {
try {
let result = await otherActor.otherFunc();
}
catch e {
// perform some action if other canister traps
doSomethingInCatch();
};
}
When does the catch block run? In which of the following cases (if any) does it run:
Canister I call with “otherActor” traps.
Canister I call with “otherActor” rejects call for some reason (message queue limit, etc.)
But if the try block fails directly with a trap (not in some async callee) the catch will not be executed and the enclosing method will, instead, immediately exit with the trap. The caller of the method will see this as a reject.
Note that message sends can, in rare cases, trap synchronously, before the message body is even queued and executed, which falls into the case of the previous paragraph.
Look at it this way: if any error happens for whatever reason in the try block, it should always go in the catch. At least that’s how a try / catch should work
Looking at the interface spec Internet Computer Content Validation Bootstrap , it says that ic0.call_perform can return an error code. So it appears that this system API call does not trap internally and that the Motoko runtime will catch any error it returns and not trap and hence execute that catch branch. However, ic0.call_new does not have a return value. So I believe that one could trap?
Can we list out precisely the scenarios, related to inter-canister calls, in which a) system API calls would trap and b) the Motoko runtime would trap, if ever?
Yes, I do this in the cycles faucet in a few places so that I have to do less error handling. But I wouldn’t put it anywhere where you are trying to squeeze everything out of your canister since the await may delay execution and I don’t know how it interacts with the message queue
Sorry, that one is not open sourced. But it’s nothing spectacular and I’m not even sure this is the proper way to do it. I use it as the lazy way out of doing more error handling like this:
case (?coupon) {
let now = Time.now();
if (coupon.expiry < now) {
throw (Error.reject("Code is expired"));
};
try {
var cycle_to_add = coupon.cycle;
let deposited = await deposit_cycles_to(cycle_to_add, wallet);
return deposited;
} catch (e) {
// Put the coupon code back if there is any error
ignore Queue.pushFront(coupon, all_coupons);
throw (e);
};
};
No, if you are ok with no error handling then you don’t have to do it. But it’s the way to add safety and prevent bugs. For example in the code above: If something fails during redeeming the faucet code I don’t want to say the code was redeemed already so I put it back into the list of valid codes. I don’t think there’s a way to do this except for try/catch (at least if you call a different canister).
What does the definition of deposit_cycles_to look like? I suppose it is a shared function of the same actor. Does deposit_cycle_to have to be public? And does this really lead to a self-call that is queued by the subnet and comes back from outside just as any externally originating call would?
It is not even shared, so it probably doesn’t. Now I am really wondering what happens if the outgoing call that happens inside deposit_cycles_to traps synchronously.
@Severin I guess what I’m asking is that can canister calls fail for a random reason? E.g. connection drops, not necessarily a bad canister? What guarantees for inter-canister calls are we working with?
AFAIK for canister calls there’s only one thing that really can be unexpected, and that is when the output queue is full. See the thread linked by @timo above for more on that.
Once the call is enqueued it will always get a response - either the success message or a response that the call trapped.