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

# Error handling

> The refusals a client meets, with a cause, a fix, and the one thing a name cannot tell you: whether retrying can ever help.

The error catalogue is **generated from the SDK's client IDL**, so it cannot drift from what the
chain actually raises. What is hand-written is the part a name cannot carry: what caused it, what
to do instead, and whether retrying is useful or an infinite loop. This page lists the refusals a
client meets. `catalogue()` holds every code the program can return.

<Warning>
  An agent that retries `RungBelowFloor` loops forever. One that gives up on `NoFreshPrice` fails
  a trade that would have landed on the next slot. **Branch on `retryable`, never on the name.**
</Warning>

## The four retry classes

| `retryable` | Meaning | What a client should do |
| - | - | - |
| `never` | The request itself is wrong. | Fix the request. Show the user what to change. |
| `after-state-changes` | The request is fine; the world is not ready, and it will get ready on its own — a clock, a settle, the price, a feed. | Re-evaluate when state moves. Back off for seconds to minutes. |
| `after-governance` | The request is fine, and only an instruction signed by the venue's config authority (`config.authority`) clears the refusal — a pause lifted, a flag set. Nothing changes on its own. | Do not retry on a timer. Poll the flag the `fix` names, back off slowly, and tell an operator. |
| `immediately` | Transient. | Retry on the next slot. These are almost all price freshness. |

Switch on all four. A client that folds `after-governance` into a default branch retries a venue-wide
stop every few seconds.

<Note>
  **The codes of orders and closes (6203 to 6238) have a finer class.** `retryClassOf(code)` returns one of
  nine classes for every code the program can raise, and `RETRY_CLASS_LABEL` has the words: for
  example "retry after the state changes", "retry with the right quote", "send the other
  instruction" and "done". `contextOf(code)` adds the note for a code whose action depends on what
  was sent. Bind to the class, not to a remembered number. The table a client meets is under
  [orders, closes and stamps](#orders-closes-and-stamps).
</Note>

## Resolving an error

```ts theme={"dark"}
import { errorForCode, errorForName, type ProgramErrorInfo } from '@rekt/1000x';

export function explain(code: number): string {
  // null for a code this program cannot produce — do not assume every custom code is ours.
  const info: ProgramErrorInfo | null = errorForCode(code);
  if (!info) return `custom program error ${code} did not come from this program`;

  return [
    `${info.name} (${info.code})`,
    info.msg ?? '',
    `Cause: ${info.cause}`,
    `Do this: ${info.fix}`,
    `Retry? ${info.retryable}`,
  ].join('\n');
}

export const byName = errorForName('NoFreshPrice');
```

## Catching a refusal

Every write path in the SDK raises `ProgramRefusal`, which carries the whole catalogue entry rather
than a string — because an interface has to render the fix next to the failure, and a caller has to
branch on `retryable` without parsing prose.

```ts theme={"dark"}
import { ProgramRefusal } from '@rekt/1000x';

export async function withRetry<T>(action: () => Promise<T>, attempts = 3): Promise<T> {
  for (let i = 0; ; i += 1) {
    try {
      return await action();
    } catch (e) {
      if (!(e instanceof ProgramRefusal)) throw e;

      switch (e.retryable) {
        case 'never':
          // The request is wrong. Retrying is a loop; surface the fix instead.
          throw new Error(`${e.info.name}: ${e.info.fix}`);
        case 'after-governance':
          // Only the config authority clears it, and nothing changes on its own. Do not retry on a
          // timer: surface the fix, which names the instruction and the key, and alert an operator.
          throw new Error(`${e.info.name}: ${e.info.fix}`);
        case 'after-state-changes':
        case 'immediately':
          break;
      }

      if (i >= attempts) throw e;

      // `immediately` is a slot away; `after-state-changes` deserves real backoff. A slot is not
      // a constant — `config.params.slotMs` is governed (`0` reads as 400) — so read it if the
      // margin matters to you.
      const waitMs = e.retryable === 'immediately' ? 400 : 2_000 * (i + 1);
      await new Promise((r) => setTimeout(r, waitMs));
    }
  }
}
```

## The refusals a client actually meets

Everything below is a real entry in the catalogue. Names, codes and messages come from the client IDL.

### Orders, closes and stamps

An open and an add are orders, and a close is a request. The rules are on
[orders and closes](/sdk/orders). These are the refusals a client meets on that path.

| Instruction | Error | What the client does |
| - | - | - |
| `open` | `SideStakeCapReached` (6224) | A limit is set on that side of the market and the side is full. Try later, or the other side. `sideRoom` shows it before the tap. |
| `open`, `add_collateral` | `TimerBelowFloor` (6225) | Use a timer of 26 seconds or more, or none. |
| `open`, `add_collateral` | `ProtocolPaused` (6046), `MarketPaused` (6047), `OracleFrozen` (6190), `SettlesDisabled` (6191) | The venue or the market is stopped. Do not retry in a loop. |
| `add_collateral` | `AddsDisabled` (6212), `EntryNotFilled` (6203), `AddPending` (6211), `ExitPending` (6213) | Adds are off; wait for the entry; wait for the earlier add; a close is on its way. |
| `request_close` | `MinimumHoldNotMet` (6103), `MarketHoldNotMet` (6236) | Wait for the hold. `closableFromSlotOf` gives the first slot. |
| `request_close` | `ExitPending` (6213) | The close is already requested. Treat it as success. |
| `request_close` | `ModeBitOff` (6228) | Close requests are switched off. Do not retry in a loop. |
| `settle` | `ExitNotAnchored` (6214), `ExitQuoteNotYetDue` (6216), `NotWindowQuote` (6217), `ExitReasonMismatch` (6215) | Request or mark first; wait for the target slot; use the quote of the current window; send the recorded reason. |
| `stamp_book` | `ProofNotAfterEntry` (6218), `ProofTooOld` (6219), `TakeProfitProofTooEarly` (6220) | That quote can never prove it: it is not after the entry, it is too old, or it is before the target was moved. |

A returned order (`OpenVoided`) and a returned add (`AddReversed`) are not refusals. They are normal
outcomes, and the stake comes back (`refundTotal`).

### Opening

An open carries no quote, so no row below can judge one: the fill judges the first price, and a price
that fails returns the order whole.

| Error | Code | Retry | What it means |
| - | - | - | - |
| `OracleFrozen` | 6190 | after-governance | The guardian has frozen the venue's oracle. Opens and adds are refused, ahead of every other row except `MinuteDisabled` on a Minute book. Only the config authority lifts it. Read `config.oracleFrozen`. |
| `ProtocolPaused` | 6046 | after-governance | Governance has stopped new positions protocol-wide. The pause stops opens and adds, not settlement. Only the config authority lifts it; the pause has no expiry. |
| `MarketPaused` | 6047 | after-governance | This one market is closed to new positions. Others may be open. Only the config authority reopens it; the pause has no expiry. |
| `MarketClosed` | 6094 | after-state-changes | The market's session calendar is shut. Opens resume when the next window opens: `availability()` reports that instant as `opensAt`, and `nextSessionOpenS(market, now)` returns it. `openGates` reports this on `open.session`. |
| `SessionEndsBeforeTenor` | 6095 | after-state-changes | The session is open, but after the market's settlement buffer less than five minutes of life is left to sell. Open earlier, or in the next session. `openTenorS` gives the life an open would get now; `openGates` reports this on `open.session`. |
| `InsideEventWindow` | 6048 | after-state-changes | A scheduled event window. Opens resume when it ends. |
| `CollateralOutOfRange` | 6049 | never | The stake is outside the governed bounds. Read them from the config account. |
| `LeverageAboveMarketTier` | 6148 | never | The `tier` argument is above this market's own. **A market's tier is a CEILING, not the only leverage it sells** — anything the engine lists at or below it opens, and `tier: 0` asks for the market's own. Read `market.tier` and offer nothing above it. |
| `LeverageBelowMinTier` | 6181 | never | The `tier` argument is below the venue's leverage floor for this market. The floor is `min(config.minTier, market.tier)` — resolve it against the market's own tier, not from `config.minTier` alone, or you will offer a tier this market refuses. `tier: 0` asks for the market's own and can never land here. |
| `RungBelowFloor` | 6116 | never | The rung is tighter than `RUNG_FLOOR_BPS` (20,000 bps = 2.00x). `rung_bps` is basis points of stake, not a whole multiple — passing `2` asks for 2 bps and lands here. |
| `RungAbovePayoutCap` | 6117 | never | The rung is above `payout_cap_multiple` — **the one take-profit ceiling**. Clamp to it, read from the config account. |
| `PositionSlotsFull` | 6056 | after-state-changes | Every slot is in use. A slot frees only when a settle for it lands: close one, or grow the book with `resizeTraderAccount`. |

A market with trading hours — see [markets](/markets) — refuses an open or an add when its session is
closed (`MarketClosed`) or too close to its end (`SessionEndsBeforeTenor`). Both clear on their own,
need no governance, and are not a mistake in the request. `availability()` says when the market opens.

### Settling

| Error | Code | Retry | What it means |
| - | - | - | - |
| `PositionNotFound` | 6057 | never | No live position carries this sequence number. Your handle is stale. |
| `NoFreshPrice` | 6007 | immediately | No quote cleared the slot age, the millisecond age and the signature floor **at the moment the program ran**. The refusal a settle meets most — [quotes and freshness](/sdk/quotes) is the page about it. Re-quote and resend; do not report it as a failed trade. |
| `SettlesDisabled` | 6191 | after-governance | The guardian has disabled settles. **Every close reason is refused**, `knockout` and `lastResort` included. Only the config authority lifts it. Poll `config.settlesDisabled`. |
| `NotPositionOwner` | 6072 | never | A `voluntary` close is authorised by the owner **or** by an unexpired session key bound to them. 6072 is raised only when no delegate account was supplied and the signer is not the owner. |
| `MinimumHoldNotMet` | 6103 | after-state-changes | `voluntary`, `takeProfit` and `userExpiry` must wait the minimum hold from the open or the last add: `resolveMinHoldSlots(config.minHoldSlots)` slots. `knockout` and `lastResort` are never gated. `tenorExpiry` is gated only on a market with a session calendar; on a market with no session calendar it is not. |
| `LastResortNotYetDue` | 6192 | after-state-changes | A `lastResort` close is legal only on a dark market (no posted price for 24 hours, `GovTable.darkAfterMin`), which needs the governance table among the remaining accounts, or once a finite frozen tenor plus `config.lastResortGraceS` has elapsed. A position with the indefinite tenor has only the dark ground. |

While the oracle is frozen, a `lastResort` close is refused `OracleFrozen` (6190) and every priced
close fails `NoFreshPrice` (6007). If `NoFreshPrice` repeats on a settle, read `config.oracleFrozen`
before you retry again.

### Payout destination

| Error | Code | Retry | What it means |
| - | - | - | - |
| `PayoutDestinationWrongMint` | 6151 | never | The destination is not a token account on the vault's mint. |
| `PayoutDestinationFrozen` | 6154 | never | The destination is frozen and could not receive a settlement. Offer an unfrozen account the trader owns. |

### Adding to a position

`add_collateral` is an open in every respect but one — it grows a slot instead of creating one — so
**every refusal in the *Opening* table above can also refuse an add**, including the guardian freeze,
the pauses and the event window. These are its own. An add carries no quote: the freshness gate
does not apply, and an add whose price is at or through a level is returned (`AddReversed`) rather
than refused.

| Error | Code | Retry | What it means |
| - | - | - | - |
| `MinCollateralOutRequired` | 6168 | never | `min_collateral_out` (open) or `min_added_collateral_out` (add) was `0`. It is **mandatory**: the caller signs a floor on the position SIZE, not a debit ceiling, and `0` is refused by name. Set it to your predicted collateral — never a token value like `1`, which satisfies the check and protects nothing. |
| `BudgetBelowCollateralFloor` | 6169 | never | The budget resolved to a smaller position than your floor — a fee rose between quote and execution. Nothing was charged. Re-quote and raise the budget, or lower the floor (never to zero). |
| `BudgetExceedsMaxCollateral` | 6170 | never | The budget buys more than `max_collateral`. Over-budget is **refused, not clamped**. Lower the budget to at most the cost of a max-collateral position; `sizeForTotal` reports that total when it refuses. |

<Warning>
  **`CollateralOutOfRange` (6049) answers three different facts on an add, and only one of them is
  worth retyping.** The minimum binds on the **increment**; the maximum binds on the **total**. So
  the code means the increment was under the floor, *or* the new total would be over the ceiling,
  *or* — the case that traps a user — the position already holds more than
  `maxCollateral − minCollateral`, so **no legal increment exists at all** and every value the
  trader types will be refused.

  A client must compute the admissible interval and render it, rather than showing one code and a
  text box. For example, with a minimum stake of $0.20 and a maximum of $1,000, a position holding
  more than **\$999.80** can take no add.
</Warning>

### Sessions, the pool and Minute

The refusals of a session key are on [session keys](/sdk/sessions#the-refusals-a-session-meets), and
`delegateGates` predicts them. A session usually ends at the token program with `InsufficientFunds`
when its allowance is spent, in none of the shapes on this page: `sessionAllowanceGates` is the only
thing that predicts it, and `renewSession` is the only call that resets all three clocks — expiry,
window and allowance — together. The refusals of an LP's redemption are on
[providing liquidity](/liquidity#getting-your-money-out), and `MinuteDisabled` is on
[integrating Minute](/minute/integrating#markets-the-venues-per-market-gate). `catalogue()` holds
every other code the program can return, each with its cause, its fix and its retry class.

## Errors that are not the program's

**Three** failure classes reach you that have nothing to do with the protocol. Conflating them
sends people to debug the wrong thing, and the first one costs the most.

<AccordionGroup>
  <Accordion title="The send you cannot confirm" icon="circle-question">
    **You broadcast a transaction and could not determine its outcome.** This is not a refusal and
    it is not a failure. It is the absence of an answer, and it is the class every client gets
    wrong first.

    **A blockhash expiring bounds how long a transaction can be ACCEPTED for. It says nothing about
    whether one already was.** So *"the blockhash expired, therefore nothing happened"* is not an
    inference — it is a guess.

    **Three rules:**

    1. **Look the signature up before you give up.** `getSignatureStatuses` searches only a node's
       *recent status cache* and answers `null` for a signature that landed but has aged out — and
       if your RPC pool races providers, `lastValidBlockHeight` can come from one node and
       `getBlockHeight` from another further ahead, expiring your hash early. **Ask
       `getTransaction`, which finds a signature wherever it is.** A hit at `confirmed` is
       definitive. Unknown is the honest answer only when it IS the answer.
    2. **Say "unknown", never "nothing happened".** A client may say *refused*, it may say
       *unknown*, and it may say *nothing moved* **only with a decoded program error code in
       hand**. Carry the signature in the message so the trader can go and look.
    3. **Keep local state on an unknown.** Delete a session secret, clear a pending record or free
       a nonce only on a decoded refusal. The branch where it landed is the expensive one — see
       [sessions](/sdk/sessions).

    **And decode before you pattern-match the text.** A client that tests the error message for
    `/reject|denied|cancel/i` before asking for the program error code will misread the program's
    own words: 6042 `RedemptionExpired` carries *cancel* in its message — *"the ticket's
    execution window has closed; cancel it and request again"* — and 6093
    `ArmedTicketNotCancellable` carries it in its name. A landed, fee-paying revert read that way
    comes back to a trader as *"you declined it in your wallet"*, which is false. **Ask for the code
    first.** A wallet decline can never carry one, because the wallet refuses before a transaction
    exists.
  </Accordion>

  <Accordion title="RPC failure" icon="plug">
    `AllProvidersFailed` when every configured endpoint failed. With `readCached` you get the last
    good value marked `stale` instead — render it with its age rather than showing a spinner that
    never resolves.
  </Accordion>

  <Accordion title="Composition failure" icon="wrench">
    `SenderRefusal` from the sender: the transaction could not be built or bound correctly. This is
    a bug in the caller or a vendor payload that does not match what the quote program expects. It
    is not a threshold and retrying does not help.
  </Accordion>
</AccordionGroup>

## The codes of the liquidity pool

The liquidity pool has refusals of its own for the cover rule, the queue and the wind-down. They are in
`catalogue()`, each with its retry class. At a fill the cover rule never refuses: an order that does
not fit is returned with the whole stake and no fee, so show "try again when the pool has room". A
transaction that lacks a capital account is a build error, and the SDK adds those accounts for you.

## Predict, do not discover

The best error handling is the error you never raise. Most of the *Opening* table above is predicted
by [`openGates`](/sdk/gates) before you spend a fee, with the value it measured and the threshold it
was judged against, and most of the *Settling* table by `settleGates` and `settlePreview`. Neither
report reads the guardian flags: read `config.oracleFrozen` and `config.settlesDisabled` yourself.

<Warning>
  **`MinimumHoldNotMet` (6103) is in the *Settling* table and neither report predicts it.** The
  gate's only input is the position's opened slot, and neither `settleGates` nor `settlePreview` reads
  that field. `settleGates` emits six rows — market pin, fresh price,
  exponent, reason legality, outflow against reserve, liquidator token — and none of them is the
  hold; `settlePreview`'s `voluntary` and `takeProfit` legality is the signer and the price, with no
  slot term in it. A client that treats green gates as "the chain accepts it" meets 6103 on chain.
  An add re-stamps the position's opened slot with the current slot, so a close within the hold of
  an add meets it as well as one within the hold of the open.
</Warning>

The *Adding* table's shared refusals are predicted by `addGates`, the report for an add.

Use the catalogue for the cases you could not predict. Use the gates for everything else.


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