> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and reverts

> Understand transaction reverts, handle failures on EVM chains and diagnose why a Sauce program stopped.

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](#try-and-catch-evm), which rolls back everything it did, or [`.catch`](#catch-on-an-external-call) on one external call. Solana has neither.

## What reverts

| Failure | Example | `sauce simulate` reports |
| - | - | - |
| `require` with a false condition | `require(amount >= 10, "below the minimum")` | `the program threw "below the minimum"` |
| `throw` | `throw "unreachable for a >= 10"` | `the program threw "unreachable for a >= 10"` |
| a call that reverts | `USDC.transfer(to, 5000000)` from an empty Pot | `"ERC20: transfer amount exceeds balance"` |
| checked arithmetic | `a + 1` with `a` at the maximum, `a - b` with `b > a`, `a / b` with `b = 0` | no reason data |
| a checked conversion | `Uint256(d)` with `d` negative ([Types](https://docs.eco.com/sauce/types.md#int256)) | no reason data |
| an index past the end of an array | `xs[5]` on a two-element array | no reason data |
| `require` without a message, `revert()` | `require(amount > 0)` | no reason data |

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.

```ts minimum.sauce.ts theme={null}
// Reverts unless `amount` is at least 10, then throws for every amount.
//
//   sauce simulate minimum.sauce.ts --chain base --owner <address> --args <amount>
//
//   amount: Uint256   any number
function main(amount: Uint256) {
  require(amount >= 10, "below the minimum");
  throw "unreachable for a >= 10";
}
```

`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.

```ts throw.sauce.ts theme={null}
function main(amount: Uint256) {
  if (amount == 0) {
    throw "zero amount";
  }
  if (amount > 1000000000) {
    revert(amount.toBytesBe(32));
  }
}
```

`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:

```bash theme={null}
sauce simulate minimum.sauce.ts --chain base --owner <address> --args 5
```

```text theme={null}
The cook reverts: the program threw "below the minimum"
```

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:

```bash theme={null}
sauce simulate minimum.sauce.ts --chain base --owner <address> --args 5 \
  --expect-revert "below the minimum"
```

```text theme={null}
As expected, the cook reverts: the program threw "below the minimum"
The cook reverts: the program threw "below the minimum"
```

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`](https://docs.eco.com/sauce-sdk/cli/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.

```ts try-catch.sauce.ts theme={null}
// Pays `to` 1 USDC, then tries to pay `amount` more while keeping `reserve` in the Pot.
// Returns 2 when both payments stand, or 1000 times `stage` plus the error's length when the
// second payment is rolled back.
//
//   sauce simulate try-catch.sauce.ts --chain base --owner <address> \
//     --args <to> <amount> <reserve>
//
//   to: Address        the recipient
//   amount: Uint256    the second payment, in USDC's smallest unit (6 decimals): 4000000 is 4 USDC
//   reserve: Uint256   the USDC the Pot must keep, in the smallest unit
function main(to: Address, amount: Uint256, reserve: Uint256): Uint256 {
  USDC.transfer(to, 1000000);
  let stage = 1;
  try {
    stage = 2;
    USDC.transfer(to, amount);
    require(pot.USDC >= reserve, "reserve too low");
  } catch (error) {
    return stage * 1000 + error.length;
  }
  return stage;
}
```

Simulated on a local fork of Base at block 52965000 with the current contracts, with `--assume-balance USDC:10` and `amount` 4 USDC:

| `reserve` | Returns | USDC moved to `to` |
| - | - | - |
| 5000000 | `2` | 1, then 4 |
| 6000000 | `1015` | 1 |

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:

```ts error-selector.sauce.ts theme={null}
// The first four bytes of the revert data when `query`, sent to `target` as a static call,
// reverts; 0 when it succeeds or reverts with fewer than four bytes.
function main(target: Address, query: bytes): Uint256 {
  require(ctx.isContract(target), "no contract");
  let selector = 0;
  try {
    evm.static(target, query);
  } catch (error) {
    if (error.length >= 4) {
      selector = error.slice(0, 4).toBe();
    }
  }
  return selector;
}
```

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:

```ts no-finally.sauce.ts theme={null}
function main(amount: Uint256) {
  try {
    require(amount > 0, "zero");
  } catch (e) {
    log("caught");
  }
  log("done");
}
```

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](#rules)).

```ts try-read.sauce.ts theme={null}
// A Uniswap V3 pool's sqrt price, or 0 when the read fails.
//
//   sauce simulate try-read.sauce.ts --chain base --owner <address> --args <pool>
//
//   pool: Address   a Uniswap V3 pool
function main(pool: Address): Uint256 {
  let price = 0;
  try {
    price = UniswapV3.Pool(pool).slot0().sqrtPriceX96;
  } catch {
    price = 0;
  }
  return price;
}
```

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:

```ts catch-statement.sauce.ts theme={null}
// Supplies `amount` of the Pot's USDC to Aave V3; returns 1 when the supply runs, 0 when it fails.
//
//   sauce simulate catch-statement.sauce.ts --chain base --owner <address> --args <amount>
//
//   amount: Uint256   in USDC's smallest unit (6 decimals): 5000000 is 5 USDC
function main(amount: Uint256): Uint256 {
  let supplied = 1;
  USDC.approve(AaveV3.Pool, amount);
  AaveV3.Pool.supply(USDC, amount, pot, 0).catch(() => {
    supplied = 0;
  });
  return supplied;
}
```

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)`:

```ts catch-parameter.sauce.ts theme={null}
function main(amount: Uint256): Uint256 {
  let size = 0;
  USDC.approve(AaveV3.Pool, amount);
  AaveV3.Pool.supply(USDC, amount, pot, 0).catch((error) => {
    size = error.length;
  });
  return size;
}
```

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>;`:

```ts catch-value.sauce.ts theme={null}
// Uniswap V3 `liquidity()` read on the chain's USDC contract, which has no such function, so the
// call reverts and the read gives 7.
//
//   sauce simulate catch-value.sauce.ts --chain base --owner <address>
function main(): Uint256 {
  const liquidity = UniswapV3.Pool(USDC).liquidity().catch(() => {
    return 7;
  });
  return liquidity;
}
```

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(...)`](https://docs.eco.com/sauce/functions#programs-built-from-a-closure) 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.

| Mistake | Compiler message | Write instead |
| - | - | - |
| the statement form on a narrow integer output: `UniswapV3.Pool(pool).liquidity().catch(() => { answered = 0; });` | ``a `.catch()` on something other than an external contract call is not supported in SauceScript yet`` | the value form: `const liquidity = UniswapV3.Pool(pool).liquidity().catch(() => { return 0; });` |
| a handler with two parameters: `.catch((e, f) => { … })` | ``a `.catch()` handler with more than one parameter (it takes the revert data) is not supported in SauceScript`` | one parameter, the revert data: `.catch((error) => { … })`, as `catch-parameter.sauce.ts` does |
| a parameter in the value form: `.liquidity().catch((e) => { return 0; })` | ``a `.catch()` fallback value with a parameter (it replaces the call's result) is not supported in SauceScript`` | no parameter: `.catch(() => { return 0; })` |
| a variable as the fallback: `.catch(() => { return fallback; })` | ``the `.catch()` fallback must be a constant scalar that fits the declared ABI result width`` | a constant: `return 7;`, as `catch-value.sauce.ts` does; or a `try` block that assigns `fallback` |
| the value form on a raw call: `const reply = evm.static(target, query).catch(() => { return 0; });` | ``a `.catch()` fallback: expected a bytes value, found a number`` | the statement form, or a `try` block, as `error-selector.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:

```ts catch-no-code.sauce.ts theme={null}
function main(target: Address): Uint256 {
  const liquidity = UniswapV3.Pool(target).liquidity().catch(() => {
    return 7;
  });
  return liquidity;
}
```

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](https://docs.eco.com/sauce/security-considerations.md) 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:

| Not supported on Solana | Compiler message | Write instead |
| - | - | - |
| a `try { … } catch (error) { … }` block | `` `try/catch` is not available on the svm target `` | a `require` before the work that could fail |
| `.catch(() => { … })` after `svm.call(program, [target], data)` | ``a `.catch()` on something other than an external contract call is not supported in SauceScript yet`` | a `require` on what the call needs, then the call |

Check what you can before the call (an account's `owner`, its `dataLen`) with `require`:

```ts call-checked.solana.sauce.ts theme={null}
function main(program: Account, target: Account) {
  require(target.owner == program.address, "target is not the program's account");
  require(target.dataLen > 0, "target holds no data");
  svm.call(program, [target], hex`00`);
}
```

## 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:

```ts try-swap.sauce.ts theme={null}
import { tryUniswapV2 } from "@sauce/router";

// Swaps `amountIn` USDC for WETH at one Uniswap V2 pair, or returns 0 with nothing moved when
// `pair` has no code (such as the zero address a factory returns for a missing pair) or cannot
// give at least `minOut`.
//
//   sauce simulate try-swap.sauce.ts --chain base --owner <address> \
//     --assume-balance USDC:<amount> --args <pair> <amountIn> <minOut>
//
//   pair: Address        a Uniswap V2 pair for USDC and WETH
//   amountIn: Uint256    the USDC to sell, in its smallest unit (6 decimals): 1000000000 is 1,000
//   minOut: Uint256      the least WETH to accept, in its smallest unit (18 decimals)
function main(pair: Address, amountIn: Uint256, minOut: Uint256): Uint256 {
  return tryUniswapV2(pair, USDC, WETH, amountIn, 0, minOut, pot);
}
```

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`](https://docs.eco.com/sauce-libraries/router#the-try-forms) 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:

```text theme={null}
╰─▶ `minimum` is not a declared variable
    ├╴`minimum` is not a declared variable (bytes 59..66)
    ╰╴error: `minimum` is not a declared variable
       --> undeclared-name.sauce.ts:2:21
        |
      2 |   require(amount >= minimum, "below the minimum");
        |                     ^^^^^^^
hint: if minimum is a value you supply, make it a parameter of main (`minimum: Uint256`, or `Address`) and pass it with --args
```

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

| Message | Cause | Fix |
| - | - | - |
| `` `minimum` is not a declared variable `` | a name nothing declares: a typo, or a value that should be a `main` parameter. A registry name the chain lacks gives its own message, naming the chain ([Names](https://docs.eco.com/sauce/names.md)) | check the spelling, or add the parameter |
| `Unexpected token` | a syntax error at that line and column, such as `function main(amount: Uint256 {` | fix the syntax: `function main(amount: Uint256) {` |
| `a function argument: expected a number, found a bool` | the called function's parameter has no annotation (`function pick(flag, amount: Uint256)`), so it is a number | annotate it: `flag: bool` |
| `` `larger` does not return on every path `` | a helper can end without a `return` (`larger` returns only when `a > b`), or a helper with no return value lacks `: void` | end it with a `return`, as `larger` does below, or annotate `: void` |
| `a nested function declaration is not supported in SauceScript` | a function declared inside another function, as `half` inside `main` | declare it at the top level, as `half` is below |
| `` `helper` is called but never declared `` | a function not in scope, usually a typo: `return helper(amount);` | fix the name |
| ``cannot combine Uint256 and Int256 in this expression; convert an operand with `Int256(value)` or `Uint256(value)` first (conversions check bounds)`` | arithmetic on a `Uint256` and an `Int256`: `amount + delta` | convert one operand: `Int256(amount) + delta` ([Types](https://docs.eco.com/sauce/types.md#int256)) |
| ``a computed member of an object the compiler holds (read it by name: `object.member`) is not supported in SauceScript yet`` | a position such as `order[0]` on an object literal | read the field by name: `order.amount` |
| ``a module-scope `let xs` must be initialized with a literal — a number, address or boolean literal. Anything computed (an expression, a call, another binding's name) has no place to be evaluated at module scope; declare it inside a function instead`` | a module-level `let` given a computed value, such as `let xs = [1, 2];` | compute it inside a function |
| `this call does not name a function, builtin, or contract method in scope` | a method called on an address in a variable or parameter: `pool.liquidity()` | name its interface: `UniswapV3.Pool(pool).liquidity()` |

Each row's fix, written together in one program that compiles:

```ts fixes.sauce.ts theme={null}
function half(x: Uint256): Uint256 {
  return x / 2;
}

function pick(flag: bool, amount: Uint256): Uint256 {
  if (flag) {
    return amount;
  }
  return 0;
}

function larger(a: Uint256, b: Uint256): Uint256 {
  if (a > b) {
    return a;
  }
  return b;
}

function main(pool: Address, useAmount: bool, amount: Uint256, delta: Int256): Uint256 {
  const order = { amount: amount, fee: 30 };
  const signed = Int256(amount) + delta;
  const liquidity = UniswapV3.Pool(pool).liquidity();
  return pick(useAmount, larger(half(order.amount), order.fee)) + Uint256(signed) + liquidity;
}
```

A message not listed here is in the reference of the construct it names: [Types](https://docs.eco.com/sauce/types.md), [Expressions and control flow](https://docs.eco.com/sauce/expressions-and-control-flow.md), [Functions](https://docs.eco.com/sauce/functions.md) and [Builtins](https://docs.eco.com/sauce/builtins.md).

## Next steps

* [Security considerations](https://docs.eco.com/sauce/security-considerations.md): the checks a program should make before it moves funds.
* [`@sauce/router`](https://docs.eco.com/sauce-libraries/router#the-try-forms) and [`@sauce/token`](https://docs.eco.com/sauce-libraries/token): the `try` swap functions and the checked token writes.
* [`sauce simulate`](https://docs.eco.com/sauce-sdk/cli/simulate): every option of the command.
* [cook()](https://docs.eco.com/sauce/cook.md): what the transaction carries and returns.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.