Error results and reverting state changes

Let’s say I have a function

shared public func doTwoThingsAndUpdateState(): async Result.Result<Nat, CustomError> {
    doThing1();
    updateState();
    doThing2();
};

The function either returns a success response or an error result.

My question is; if an error result is returned, does that automatically revert any state changes that happened? Lets say the error was detected after the doThing2() function. Are any state changes before that automatically reverted just by returning the error response?

Coming from a Solidity background so wondering if this is equivalent to the revert() keyword in Solidity, which explicitly tells the EVM to revert any state changes.

1 Like

Unless there is a trap/error, nothing should be rolled back. Returning a Result with error data does not do anything unless you explicitly handle rolling back state changes yourself
They bring said, a trap will only rollback to the last canister call, if there are any in your method

Either the code is run, or it traps

1 Like

Equivalent to revert() would be Debug.trap(“some message”) or assert false statements.

But you have to do it before the first await statement (if you have any).

1 Like

That is not quite correct. It rolls back to the last await if any. Canister calls can occur without await (and vice versa), and do not delimit a rollback.

1 Like

Ok gotcha. So in asynchronous functions the entire function is not treated like one atomic transaction (in the database sense), where its all or nothing.
Also read more about this in the docs so better understand how this works.

How to handle when making multiple external canister calls, for example swapping tokens which requires multiple token transfers, where it’s possible one succeeds, but a later call fails? In that case the canister could be left in an inconsistent state.

So rolling back would require transferring tokens back. Going back to my example, but to be more concrete:

  public shared func doTwoThingsAndUpdateState(params1, params2): async Result.Result<Nat, CustomError> {
    let result1 = await token1.transfer_from(params1);
    if (not checkResult(result1)) {
      throw Error.reject("transfer 1 failed");
    };
    let result2 = await token2.transfer_from(params2);
    if (not checkResult(result2)) {
      // reverse the first transfer
      token1.transfer(params_to_reverse);
      throw Error.reject("transfer 2 failed");
    };
    
    #ok(1);
};

So essentially is this the pattern used to handle scenarios like this?