Your Error Codes Decide Whether Bots Can Retry

A caller cannot tell a permanent failure from a transient one unless your errors distinguish them. What that costs them, and how to design for it.

BoltTx Team··8 min read
solanaerror-handlinganchorprogram-developmentretrytransaction-landing

Program errors are usually written for the developer reading logs. ★The primary consumer is a bot deciding, in the next slot, whether to try again.★

That bot cannot read your error message. It can read the code, and everything it does next follows from what the code tells it.

The Only Distinction That Matters to a Caller

Every failure a caller sees falls into one of two categories:

Category Correct response Example
★Transient★ ★Retry with the same intent★ Slippage exceeded, stale price
★Permanent★ ★Rebuild or abandon★ Unauthorised, account closed, invalid parameter

★If your errors do not separate these, callers must guess.★ And the two guesses fail in opposite directions:

Assume transient — a bot retries an unauthorised call until the blockhash expires, paying fees for something that could never succeed.

Assume permanent — a bot abandons a trade that would have worked one slot later.

Neither is recoverable by the caller. The information only exists in your program.

Anchor Numbers From 6000

#[error_code]
pub enum SwapError {
    SlippageExceeded,        // ★6000★
    PoolNotInitialized,      // 6001
    Unauthorized,            // 6002
}

The order of variants is the numbering, which produces a compatibility hazard worth knowing about:

#[error_code]
pub enum SwapError {
    SlippageExceeded,        // 6000
    NewErrorInserted,        // ★6001 — shifts everything below★
    PoolNotInitialized,      // ★now 6002, was 6001★
    Unauthorized,            // ★now 6003, was 6002★
}

★Inserting a variant renumbers every error after it.★ A caller with 6001 = retry hardcoded now retries on a completely different condition, and nothing in the transaction tells them the meaning changed.

Append new variants at the end. Never insert, never reorder. It costs nothing and it is irreversible if you get it wrong.

If your program's errors are clear and transactions still miss, a free BoltTx key is one line for your users to test the submission path.

Group by Retryability, Not by Subsystem

The organising principle that helps callers most:

#[error_code]
pub enum SwapError {
    // ★6000-6099: transient — retrying may succeed★
    SlippageExceeded = 0,
    PriceStale = 1,
    InsufficientLiquidityNow = 2,

    // ★6100-6199: permanent — retrying cannot succeed★
    Unauthorized = 100,
    PoolClosed = 101,
    InvalidTokenPair = 102,
}

★A caller can then branch on a range instead of maintaining a list of your individual codes.★

const code = parseCustomError(err);
if (code >= 6000 && code < 6100) return retry();
if (code >= 6100) return abandon();

This survives you adding errors, because a new transient error lands in the transient range and existing caller logic handles it correctly without an update.

Make Errors Say What Went Wrong

// ★Useless to a caller.★
require!(valid, SwapError::InvalidInput);

// ★Actionable.★
require!(amount >= MIN_AMOUNT, SwapError::AmountBelowMinimum);
require!(amount <= max_for_pool, SwapError::AmountExceedsPoolCapacity);
require!(deadline > clock.unix_timestamp, SwapError::DeadlinePassed);

★Three specific errors let a bot resize, wait, or abandon respectively. One generic error forces it to abandon all three.★

The cost of a vague error is not confusion — it is a caller taking the most conservative action available, which is usually giving up on a trade that a smaller size would have completed.

Distinguish "Not Yet" From "Never"

This is the distinction bots most often lack, and it is cheap to provide:

// ★Ambiguous: is the pool broken, or just empty right now?★
require!(pool.liquidity > 0, SwapError::NoLiquidity);

// ★Clear.★
require!(!pool.is_closed, SwapError::PoolPermanentlyClosed);   // ★never★
require!(pool.liquidity > 0, SwapError::PoolEmptyNow);         // ★not yet★

A bot seeing PoolEmptyNow can keep the pool in its watch list. A bot seeing NoLiquidity has no basis for deciding whether to ever look again.

★For anything a caller polls or retries against, this pair of errors is worth more than any amount of logging.★

Document the Codes, Not Just the Names

The IDL carries variant names, which helps a developer and not a bot at runtime. What callers actually need:

6000  SlippageExceeded          ★retry — price moved★
6001  PriceStale                ★retry — refresh and resubmit★
6100  Unauthorized              ★permanent — check signer★
6101  PoolClosed                ★permanent — remove from watchlist★

★Publishing the retryability alongside the code is the difference between a caller implementing correct behaviour and a caller guessing.★

Most programs publish neither, which is why most bots treat every program error identically — usually by abandoning, which costs their users trades that would have worked.

What Landing Looks Like

Real transactions through our delivery nodes: median confirmation 336ms — under one slot.

★A program error means the transaction landed and reverted — the base fee was paid.★ Good error design does not prevent that cost, but it prevents the caller from paying it repeatedly on a condition that will never clear.

Where BoltTx Fits

We handle submission for the people calling your program. Error semantics are decided entirely in your program.

Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a transaction is not observable in transit before it lands.

Your callers sign locally. We never hold funds, never sign, and never modify transaction contents. The tip travels inside the transaction, paid on chain from their own wallet, and reverts with the transaction if it fails, because that is how Solana handles atomic transactions. They pay only on transactions that reach the chain.

Get a free API key. No monthly fee:

const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

FAQ

Why do program error codes matter for trading bots? Because a bot decides whether to retry based on the code alone. If your errors do not distinguish transient from permanent failures, the bot must guess, and both guesses are expensive.

Where do Anchor error codes start? At 6000, numbered by the order of variants in the enum. That ordering is the wire format, which is why inserting a variant is a breaking change.

What happens if I insert a new error variant? Every error after it is renumbered. Callers with hardcoded codes now interpret a different condition, and nothing in the transaction signals that the meaning changed.

How should I organise error codes? By retryability rather than by subsystem. Reserving ranges lets callers branch on a range instead of maintaining a list, and it survives you adding new errors.

What is the difference between transient and permanent errors? Transient means retrying the same intent may succeed, such as slippage. Permanent means it cannot, such as an unauthorised signer. Callers need this distinction and cannot derive it.

Why is a generic error code harmful? Because it forces the caller into the most conservative response. Three specific errors let a bot resize, wait, or abandon, while one generic error makes it abandon in all three cases.

Should I separate "empty now" from "closed forever"? Yes. A bot can keep polling a temporarily empty pool but should remove a permanently closed one from its watchlist. One combined error gives it no basis to decide.

Do error messages help bots? No, only codes reach them programmatically. Messages help a developer reading logs, so put the actionable information in the code and its documented meaning.

How do I document error codes usefully? Publish the number, the name, and whether retrying can succeed. The IDL carries names but not retryability, which is the part callers actually need at runtime.

Can I reuse an error code after removing a variant? No. Leave a gap instead. Reusing a number means old callers interpret the new condition as the old one, which is worse than an unknown code.

How do callers read a custom error? From the transaction error as a custom program error number. They subtract 6000 for the Anchor variant index, or match against the ranges you documented.

What is error 0x1771? 6001 in decimal, which is the second variant of some Anchor program. The number alone means nothing without that program's error documentation.

Should errors include values, such as the required amount? Anchor supports messages, but they are not machine-readable. If a caller needs a number to act on, expose it through account state rather than through the error.

How many error variants should a program have? Enough that each one implies a distinct caller response. Two errors that lead to the same action could be one, and one error covering two actions should be two.

Do error codes affect compute usage? Negligibly. The cost is in the checks themselves rather than in the error definitions, so specificity is essentially free.

What is the most common error design mistake? A single generic validation error covering many conditions, followed closely by inserting variants mid-enum and silently renumbering everything after it.

Back to all posts