Continuations
A deferred call runs after the transition that defers it. So how do you use its result? Attach a continuation with then:
interface Exchange is {
function swap(amountIn : uint) : uint;
}
event Swapped of record {
indexed id : uint;
amountOut : uint;
}
type Order is record {
status : uint8;
amountOut : uint;
}
storage is record {
exchange : Exchange;
orders : map(uint, Order);
}
external nonreentrant function execute(const id : uint; const amountIn : uint) is {
storage.orders[id].status := uint8(1);
defer storage.exchange.swap(amountIn)
then (amountOut : uint) {
storage.orders[id].status := uint8(2);
storage.orders[id].amountOut := amountOut;
emit Swapped { id = id; amountOut = amountOut; };
};
}Phases
A continuation is a new transition phase:
- The first phase runs: it marks the order pending and records the swap.
- It commits. The exchange runs and sees the order as pending, with the first phase's state final and consistent.
- The swap's result is decoded and checked against its declared type.
- The continuation runs as its own phase. It reads storage again, so it sees anything the exchange changed, and then it writes, emits, and may defer more calls.
Continuations run depth-first: a continuation's own deferred calls, and their continuations, finish before the next call deferred by the phase above it.
Binding the result
| Form | Use |
|---|---|
then { ... } | run after the call and ignore its result |
then (x : T) { ... } | bind the result |
then ((a, b) : (T, U)) { ... } | destructure a tuple result |
The binding's type must match the method's declared return type.
interface Vault is {
function withdraw(shares : uint) : (uint, uint);
function deposit(amount : uint) : uint;
}
storage is record {
vault : Vault;
principal : uint;
earned : uint;
shares : uint;
}
external nonreentrant function redeem(const shares : uint) is {
defer storage.vault.withdraw(shares)
then ((principal, earned) : (uint, uint)) {
storage.principal := storage.principal + principal;
storage.earned := storage.earned + earned;
defer storage.vault.deposit(earned)
then (newShares : uint) {
storage.shares := storage.shares + newShares;
};
};
}What crosses a phase boundary
A continuation can use the enclosing function's parameters and const bindings. They are values and cannot have changed. It cannot use a var from an earlier phase, because the external call sits between the two phases:
interface Exchange is {
function swap(amount : uint) : uint;
}
storage is record {
left : uint;
}
external nonreentrant function execute(const ex : Exchange; const amount : uint) is {
var remaining : uint := amount;
defer ex.swap(amount)
then (out : uint) {
storage.left := remaining + out;
};
}`remaining` is a `var` of an earlier phase; mutable locals do not cross an external callStorage is never carried across: a continuation always reads current storage.
Failure
If the call reverts, the transaction reverts, including the first phase. A continuation can assert and fail like any other code, and its errors count toward the enclosing function's raises.
Recovering from failed calls
To handle a failed call instead of reverting, give the continuation an Ok arm and an Error arm:
interface Registry is {
function notify(id : uint) : bool;
}
storage is record {
registry : Registry;
notified : map(uint, bool);
failures : uint;
lastError : bytes;
}
external nonreentrant function publish(const id : uint) is {
defer storage.registry.notify(id) then {
| Ok(accepted : bool) -> {
storage.notified[id] := accepted;
}
| Error(reason : bytes) -> {
storage.failures := storage.failures + 1;
storage.lastError := reason;
}
};
}Ok(x : T)binds the decoded result. For a method that returnsunit, writeOkwith no binding.Error(reason : bytes)binds the raw revert data, andError(_)ignores it.forward reasonreverts with exactly those bytes, passing the callee's failure on unchanged. It is not one of the contract's declared errors, so the function raisesforwarded.reason.lengthis its length in bytes, soif reason.length = 0 then { fail MyError; } forward reason;replaces only an empty reason.- Both arms are required:
a recoverable call needs both `Ok` and `Error` arms. - Each arm is a new phase, like any continuation. The failed call's own effects are undone, but the first phase's writes stay.
- A call that succeeds but returns data that does not decode as
Tstill reverts the transaction, as does no data for an optional result from an account without code. It is not anError.
defer Evm.transfer(to, amount) recovers the same way, with Ok and Error(_) arms.
A query recovers with an ordinary case, because its result is available immediately:
interface Oracle is {
view function price(asset : address) : uint;
}
storage is record {
oracle : Oracle;
fallbackPrice : uint;
}
external view function priceOrFallback(const asset : address) : uint is
case query storage.oracle.price(asset) of {
| Ok(price) -> price
| Error(_) -> storage.fallbackPrice
}Continuations and reentrancy
While the call runs, your contract can be re-entered. Declare functions with continuations nonreentrant unless you have a reason not to, so that no other nonreentrant function can run between the phases. See Reentrancy.

