try block, which rolls back everything it did, or .catch on one external call. Solana has neither.
What reverts
Each row was simulated on a local fork of Base at block 52965000 with the current contracts.
Token writes through
@sauce/token (USDC.transfer, Token(address).approve, transfer(token, to, amount)) check the token’s reply as OpenZeppelin’s SafeERC20 does. A token that returns false, a reply that is not one word equal to 1, and empty return data from an address with no code each revert the cook. A token that returns nothing, such as USDT, is accepted. approve first resets a nonzero allowance to zero.
require
require(cond) reverts unless cond is true or nonzero. require(cond, message) reverts with message, a bytes value such as "below the minimum", as the revert reason.
minimum.sauce.ts
require is a statement. It produces no value, and using it as one is a compile error.
throw and revert
throw "reason" reverts with "reason" as the revert data. It is the same as revert("reason"). revert() reverts with no data, and revert(data) with any bytes, such as an encoded error code.
throw.sauce.ts
throw new Error("zero amount") is not supported: the compile fails with `new` on anything other than `Array` (only `new Array(n)` is supported) is not supported in SauceScript. Throw the bytes directly, as throw.sauce.ts does: throw "zero amount";.
How a revert surfaces in sauce simulate
sauce simulate runs the cook against the chain’s state, signs nothing, and decodes the revert reason. It exits with status 1 when the cook reverts:
--expect-revert <text>, the command exits 0 only if the cook reverts with a reason containing <text>, so a script or a test can assert a guard:
--args 12, the require passes and the throw runs: The cook reverts: the program threw "unreachable for a >= 10". A revert with no data prints it reverted with no data (out of gas, or a revert with no reason); give every require a message so a failure names its cause. These runs used a local fork of Base at block 52965000 with the current contracts. sauce simulate lists the command’s options.
try and catch (EVM)
A try block runs its statements as one unit. If none fails, everything it did stands. If one fails, every call, storage write, array or record change and variable assignment made in the block is undone, and then the catch block runs. Work done before the try is kept.
try-catch.sauce.ts
--assume-balance USDC:10 and amount 4 USDC:
With a 6 USDC reserve, the
require fails after the 4 USDC transfer. The transfer is undone, and stage is back to 1 when the handler reads it: a JavaScript catch would see 2. The 1 USDC paid before the try stays paid. error.length is 15, the bytes of "reserve too low": a program’s require message is its revert data as it is, with no ABI encoding.
catch (error)binds the failure’s revert data as bytes, at most the first 256 of them. They may be empty, or cut short. Check their length before reading them:
error-selector.sauce.ts
target the chain’s USDC and query the calldata of transfer(<address>, 1) from a Pot that holds none, simulated on the same fork, this returns 147028384: 0x08c379a0, the selector of Solidity’s Error(string).
catch { … }, with no binding, discards the data.return,breakandcontinuemay leave atryblock or a handler.- A failure inside the handler is not caught by that handler; it reverts the cook unless an enclosing
trycatches it. - There is no
finally: afinally { … }block fails witha `finally` block is not supported in SauceScript. Put the statements after thetry/catch, where they run after either path unless the block or the handler returns:
no-finally.sauce.ts
try around one external call and nothing else is that call’s .catch, with its narrower boundary (next section). To catch a failure in decoding a call’s result, or in a checked operation on it, put that work in the block too. The program below reads a pool’s price with try. A .catch cannot do it: slot0() has several outputs, and its statement form is refused for narrow integer outputs (see Rules).
try-read.sauce.ts
0 for the chain’s USDC contract, which has no slot0(), and for an address with no code.
.catch on an external call
.catch(() => { … }) after a call to another contract runs its block when that call reverts. The call’s own effects are undone; everything before it in the program stands. The block may read and assign the enclosing function’s variables.
The statement form runs the block on failure:
catch-statement.sauce.ts
--assume-balance USDC:10: amount 5 USDC returns 1, and the Pot pays 5 USDC and receives 4.999999 aBasUSDC. amount 20 USDC returns 0 and moves no USDC, and the approval of 20 USDC, made before the call, stays.
The handler may take one parameter, the revert data as bytes, with the 256-byte limit of catch (error):
catch-parameter.sauce.ts
amount 20 USDC on the same fork, this returns 132: the ABI-encoded Error(string) carrying USDC’s “ERC20: transfer amount exceeds balance”.
The value form gives a constant when the call fails. It takes a call with one output, and its block is exactly return <constant>;:
catch-value.sauce.ts
main returns Uint256: 7. The same program without .catch reverts.
Rules
.catchworks on an external call on an EVM chain: a namespace contract’s method, an imported ABI’s method,evm.call,evm.staticandevm.delegate.- The handler is a
{ … }block, never() => value. A.catchhandler and aprogram(...)closure are the only arrow functions the language accepts. - In the value form, the handler takes no parameter, and the fallback is a constant that fits the call’s declared result. A raw
evm.call,evm.staticorevm.delegatereturns bytes, which no constant stands in for: test a raw call with the statement form or atryblock. - A handler that is too large fails the compile; move the work into a function and call it.
- The statement form can be refused for a method whose outputs include a narrow integer, such as
liquidity(), which returns auint128, orslot0(). The message does not name the cause. For a method with one output, use the value form, ascatch-value.sauce.tsdoes withliquidity(). For several outputs, use atryblock, astry-read.sauce.tsdoes.
An address with no code
A call to an address with no code does not revert on an EVM chain. It succeeds with no return data..catch covers the call’s failure only, not the decoding of what it returned, so decoding that empty reply reverts the cook:
catch-no-code.sauce.ts
target an address with no code, the cook reverts with no reason data. Two programs handle that address:
- with
require(ctx.codeSize(target) > 0, "no contract at the address");before the call, the same simulation reverts withthe program threw "no contract at the address"; - with
try { liquidity = UniswapV3.Pool(target).liquidity(); } catch (error) { liquidity = 7; }, where the assignment is inside the block, the program returns7.
Not on Solana
On Solana a failed program call fails the whole transaction.try and .catch are both refused there:
Check what you can before the call (an account’s
owner, its dataLen) with require:
call-checked.solana.sauce.ts
Swaps that may not fill: the try functions
Every @sauce/router swap function on an EVM chain has a try form with the same arguments (tryUniswapV2, tryUniswapV3, tryCurve, tryBalancerV2 and the rest). Where the try form can tell before the swap commits that it would fail, it returns 0 having moved nothing, so a program can fall back to another pool instead of reverting. Not every failure is one it checks for:
try-swap.sauce.ts
--assume-balance USDC:1000 and amountIn 1,000 USDC: the Uniswap V2 USDC/WETH pair with a minOut of 1 returns 418305883082737258 (0.418 WETH), and with a minOut of 10^30 returns 0 with no USDC moved. 0x…dEaD and the zero address, which have no code, return 0. Base’s USDC contract, which has code and is not a pair, reverts the cook with no data.
- Every
tryform returns0for a pool, pair or Vault with no code. tryUniswapV2andtryTraderJoeLBread the pair’s view uncaught: a contract that has code but is not a V2 or LB pair, such as a token, reverts the cook.- The forms that check the pool’s quote before paying it (
tryUniswapV2,tryTraderJoeLB,tryDodoV2,tryWoofi) still revert on a failure the check does not foresee. - The forms through the Router (
tryUniswapV3,tryUniswapV4,tryMaverickV2,tryInfinityCL,tryInfinityBin) revert when a swap ran and delivered less thanminOut. - Solana has no
tryforms.
@sauce/router describes each try function.
Compile errors
A program that does not compile fails the command; the compiler has no warnings channel.sauce compile prints the message, the file, the line and the column, and for some errors a hint: line. For a main whose body is require(amount >= minimum, "below the minimum"); with minimum never declared, it prints:
USDC.transfer, a protocol namespace) and adds imports at the top. An error on such a line names main.js, the program as rewritten, and a note: line says so; its line number counts the added imports, so it can be past the line in your file. The messages below are the ones the sauce command prints for the mistakes their rows name.
Common messages
Each row’s fix, written together in one program that compiles:
fixes.sauce.ts
Next steps
- Security considerations: the checks a program should make before it moves funds.
@sauce/routerand@sauce/token: thetryswap functions and the checked token writes.sauce simulate: every option of the command.- cook(): what the transaction carries and returns.
