Skip to main content
A Sauce program runs inside one transaction, and a failure is a revert. An unhandled failure reverts the whole cook: every transfer, approval and call made earlier in the same cook is undone, and the transaction fails. Approvals and transfers made by earlier transactions are outside that rollback. On an EVM chain a program can handle a failure in two ways: a 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:
The output then lists the steps it simulated: the Pot’s deploy, when the owner has no Pot yet, and the cook. With --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:
With --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
Simulated on a local fork of Base at block 52965000 with the current contracts, with --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
With 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, break and continue may leave a try block or a handler.
  • A failure inside the handler is not caught by that handler; it reverts the cook unless an enclosing try catches it.
  • There is no finally: a finally { … } block fails with a `finally` block is not supported in SauceScript. Put the statements after the try/catch, where they run after either path unless the block or the handler returns:
no-finally.sauce.ts
A 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
On the same fork it returns the WETH/USDC 0.05% pool’s price for that pool, and 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
Simulated on a local fork of Base at block 52965000 with the current contracts, with --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
With 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
On the same fork this prints main returns Uint256: 7. The same program without .catch reverts.

Rules

  • .catch works on an external call on an EVM chain: a namespace contract’s method, an imported ABI’s method, evm.call, evm.static and evm.delegate.
  • The handler is a { … } block, never () => value. A .catch handler and a program(...) 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.static or evm.delegate returns bytes, which no constant stands in for: test a raw call with the statement form or a try block.
  • 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 a uint128, or slot0(). The message does not name the cause. For a method with one output, use the value form, as catch-value.sauce.ts does with liquidity(). For several outputs, use a try block, as try-read.sauce.ts does.

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
Simulated on a local fork of Base at block 52965000 with the current contracts, with 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 with the 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 returns 7.
A call whose result the program never reads is not decoded at all, so to an address with no code it succeeds. Security considerations covers when to check for code.

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
Simulated on a local fork of Base at block 53005062 with the current contracts, with --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 try form returns 0 for a pool, pair or Vault with no code.
  • tryUniswapV2 and tryTraderJoeLB read 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 than minOut.
  • Solana has no try forms.
@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:
The package rewrites some lines before compiling (a token symbol, 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
A message not listed here is in the reference of the construct it names: Types, Expressions and control flow, Functions and Builtins.

Next steps