The only way to catch an exception is to have a callback on the promise that generated the exception.
In the explained scenario, the contractA.callback()
shouldn't crash. You need to construct the contract carefully enough to avoid failing on the callback. Most of the time it's possible to do, since you control the input to the callback and the amount gas attached. If the callback fails, it's similar to having an exception within an exception handling code.
Also note, that you can make sure the callback
is scheduled properly with the enough gas attached in contractA.run()
. If it's not the case and for example you don't have enough gas attached to run
, the scheduling of callback and other promise will fail and the entire state from run
changes is rolled back.
But once run
completes, the state changes from run
are committed and callback
has to be carefully processed.
We have a few places in lockup
contract where the callback is allowed to fail: https://github.com/near/core-contracts/blob/6fb13584d5c9eb1b372cfd80cd18f4a4ba8d15b6/lockup/src/owner_callbacks.rs#L7-L24
And also most of the places where the callback doesn't fail: https://github.com/near/core-contracts/blob/6fb13584d5c9eb1b372cfd80cd18f4a4ba8d15b6/lockup/src/owner_callbacks.rs#L28-L61
To point out there are some situation where the contract doesn't want to rely on the stability of other contracts, e.g. when the flow is A --> B --> A --> B
. In this case B
can't attach the callback to the resource given to A
. For these scenarios we were discussing a possibility of adding a specific construct that is an atomic and has a resolving callback once it's dropped. We called it Safe
: https://github.com/nearprotocol/NEPs/pull/26
EDIT
What if contractB.run
fails and I will like to update the state in contractA
to rollback changes from contractA.run
?
In this case contractA.callback()
is still called, but it has PromiseResult::Failed
for its dependency contractB.run
.
So callback()
can modify the state of contractA
to revert changes.
For example, a callback from the lockup contract implementation to handle withdrawal from the staking pool contract: https://github.com/near/core-contracts/blob/6fb13584d5c9eb1b372cfd80cd18f4a4ba8d15b6/lockup/src/foundation_callbacks.rs#L143-L185
If we adapt names to match the example:
The lockup contract (contractA
) tries to withdraws funds (run()
) from the staking pool (contractB
), but the funds might still be locked due to recent unstaking, so the withdrawal fails (contractB.run()
fails).
The callback is called (contractA.callback()
) and it checks the success of the promise (of contractB.run
). Since withdrawal failed, callback reverts the state back to the original (reverts the status).
Actually, it's slightly more complicated because the actual sequence is A.withdraw_all -> B.get_amount -> A.on_amount_for_withdraw -> B.withdraw(amount) -> A.on_withdraw