Errors
Lithe has no string reverts. Every intentional failure is a declared error type, and every external function declares the errors it can raise. The compiler checks that nothing is missing.
Declaring errors
Errors take one of three forms:
(* No payload. In the ABI: Unauthorized() *)
error Unauthorized;
(* One value. In the ABI: TooLarge(uint256) *)
error TooLarge of uint;
(* Named fields. In the ABI: InsufficientBalance(uint256 available, uint256 required) *)
error InsufficientBalance of record {
available : uint;
required : uint;
}
(* Strings, bytes, and lists too. In the ABI: UnknownKey(string) *)
error UnknownKey of string;
(* In the ABI: AttributeNotFound(uint256 tokenId, string traitType) *)
error AttributeNotFound of record {
tokenId : uint;
traitType : string;
}
storage is record {
n : uint;
}
external function check(const n : uint)
raises TooLarge
is {
assert(n <= 100, TooLarge(n));
storage.n := n;
}
external function lookup(const tokenId : uint)
raises UnknownKey | AttributeNotFound
is {
assert(tokenId > 0, UnknownKey("token 0"));
assert(tokenId <= storage.n, AttributeNotFound { tokenId = tokenId; traitType = "Background"; });
}Each error compiles to a Solidity custom error. The ABI name is the declared name, with the selector Solidity would compute. Payloads are any ABI values, ABI-encoded as Solidity would. A record payload is the error's parameters, one per field. Two errors whose selectors collide, such as errors of one name and payload from two imported files, are rejected.
Raising errors
assert raises when its condition is false. fail always raises. The error is written the way it was declared:
import "std/evm";
error Unauthorized;
error TooLarge of uint;
error InsufficientBalance of record {
available : uint;
required : uint;
}
storage is record {
owner : address;
balances : map(address, uint);
}
external function withdraw(const amount : uint)
raises Unauthorized | TooLarge | InsufficientBalance
is {
assert(Evm.sender() = storage.owner, Unauthorized);
assert(amount <= 1_000_000, TooLarge(amount));
const have : uint := storage.balances[Evm.sender()];
if have < amount then {
fail InsufficientBalance { available = have; required = amount; };
}
storage.balances[Evm.sender()] := have - amount;
}A record payload can also be passed as one value: with error BadPoint of Point, where Point is a record type, fail BadPoint(p) raises it for a p : point. The record's fields are still the error's parameters.
raises
An external function must list every error its body can raise, directly or through the functions it calls:
import "std/evm";
error NotOwner;
error Closed;
storage is record {
owner : address;
open : bool;
}
external function go()
raises NotOwner
is {
assert(Evm.sender() = storage.owner, NotOwner);
assert(storage.open, Closed);
}external function `go` may raise `Closed`If raises is missing entirely, the compiler says what it inferred:
main.lithe:7:10: external function `go` must declare its errors
inferred:
NotOwnerA function's ABI, its raises clause, and the output of lithe errors therefore always agree. Callers and auditors can rely on the full set of failures.
Internal functions may leave raises off. Their errors are inferred and propagate to their callers:
import "std/evm";
error NotOwner;
storage is record {
owner : address;
value : uint;
}
view function onlyOwner() is {
assert(Evm.sender() = storage.owner, NotOwner);
}
external function set(const value : uint)
raises NotOwner
is {
onlyOwner();
storage.value := value;
}raises none declares that a function raises no declared errors. Panics are still possible.
Forwarded failures
A failed call's revert data can be passed on unchanged with forward reason, where reason is the bytes from its Error arm. ERC-721's and ERC-1155's safe transfers require it. The data is not one of the contract's errors, so a function that forwards lists forwarded in its raises, and so does every function that calls it:
error Rejected;
interface Receiver is {
function accept(amount : uint) : bool;
}
storage is record {
receiver : Receiver;
}
external function notify(const amount : uint)
raises Rejected | forwarded
is {
defer storage.receiver.accept(amount) then {
| Ok(accepted : bool) -> {
assert(accepted, Rejected);
}
| Error(reason : bytes) -> {
if reason.length = 0 then {
fail Rejected;
}
forward reason;
}
};
}fail takes only declared errors, and forward only bytes. lithe errors lists forwarded with the function's other errors. forwarded is not an error name.
Error sets
Name a group of errors once and reuse it:
error Paused;
error TooSmall of uint;
error Insufficient of uint;
errors withdrawErrors is
| Paused
| TooSmall
| Insufficient
storage is record {
paused : bool;
balance : uint;
}
external function withdraw(const amount : uint)
raises withdrawErrors
is {
assert(not storage.paused, Paused);
assert(amount >= 10, TooSmall(amount));
assert(storage.balance >= amount, Insufficient(storage.balance));
storage.balance := storage.balance - amount;
}A raises clause can mix sets and errors: raises withdrawErrors | NotOwner. Sets cannot contain other sets.
Panics
Some failures are not declared errors. They are violated invariants, and they revert with Solidity's Panic(uint256):
| Code | Cause |
|---|---|
0x11 | arithmetic overflow or underflow, a conversion out of range, or a value too wide for Uint.toHexString's length |
0x12 | division or remainder by zero |
0x32 | list index out of range |
An arithmetic operation on constants that must fail is a compile error instead of a panic, as Arithmetic describes.
A failed query or deferred call also reverts the transaction, unless the code recovers from it with Ok and Error arms. A non-payable function reverts if it receives ether, and then the ABI decoder reverts on malformed calldata: every argument is checked before the body runs, including its guard, whether the body reads the argument itself, passes it on, or calls a delegated module with it. None of these appear in raises.
Inspecting errors
lithe errors lists every entrypoint's errors, and every error's selector, fields, and raisers:
lithe errors bank.lithewithdraw(uint256)
Paused()
InvalidAmount(uint256)
InsufficientBalance(uint256,uint256)
...
InsufficientBalance(uint256,uint256)
selector:
0xcf479181
fields:
available : uint256
required : uint256
raised by:
withdraw(uint256)
