Recovering from failed calls
By default, a failed external call reverts the whole transaction. To handle the failure instead, match the outcome with Ok and Error arms. A deferred call matches in its continuation, and a query matches with case.
try-catch.lithe
import "std/evm";
interface Oracle is {
view function price(asset : address) : uint;
}
interface Hook is {
function onPayout(amount : uint) : bool;
}
event PayoutFailed of record {
indexed to : address;
amount : wei;
}
storage is record {
oracle : Oracle;
lastGoodPrice : uint;
hook : Hook;
hookFailures : uint;
unpaid : map(address, wei);
}
(* A query's outcome is available now, so it matches with an ordinary `case`. *)
external function refresh(const asset : address) is {
const price : uint := case query storage.oracle.price(asset) of {
| Ok(p) -> p
| Error(_) -> storage.lastGoodPrice
};
storage.lastGoodPrice := price;
}
(* A deferred call's outcome arrives in its continuation. `Error` binds the raw
revert data. *)
external nonreentrant function notify(const amount : uint) is {
defer storage.hook.onPayout(amount) then {
| Ok(accepted : bool) -> {
skip
}
| Error(reason : bytes) -> {
storage.hookFailures := storage.hookFailures + 1;
}
};
}
(* A transfer that fails is recorded for later, instead of blocking the caller.
`Ok` has no binding because a transfer returns nothing. *)
external payable nonreentrant function pay(const to : address) is {
defer Evm.transfer(to, Evm.value()) then {
| Ok -> { skip }
| Error(_) -> {
storage.unpaid[to] := storage.unpaid[to] + Evm.value();
emit PayoutFailed { to = to; amount = Evm.value(); };
}
};
}- Both arms are required. With neither, the call is not recoverable, and a failure reverts.
- The first phase's writes stay whichever arm runs. Only the failed call's own effects are undone.
- A call that succeeds but returns data that doesn't decode as the declared type still reverts. That's a broken interface, not an
Error. - See Continuations for the full rules.

