How to handle errors and reverts

This guide shows you how to branch on the errors the SDK throws so a bot or a UI reacts correctly to a bad input, an outage, and a rejected transaction. It assumes you write try/catch in TypeScript. The complete list of classes and contract error names is in Errors.

Branch on the class, not the message

Every SDK error has a concrete class. Test with instanceof, most specific first, and keep a final throw for errors the SDK does not own. InvariantError is the class for an SDK defect; report it and rethrow it instead of using it for control flow.

ts
import {
  ContractRevertError,
  IndexerError,
  IndexerWaitTimeoutError,
  InvariantError,
  InvalidInputError,
  RpcError,
  SignerRequiredError,
  SomniaMarketsError,
} from "@somnia-chain/markets-sdk";

try {
  await exchange.createOrder("SOMI/USDso", "limit", "buy", 10, 0.11);
} catch (e) {
  if (e instanceof ContractRevertError) {
    // the chain rejected it: read e.errorName
  } else if (e instanceof InvalidInputError) {
    // the call is wrong: fix the arguments, do not retry
  } else if (e instanceof SignerRequiredError) {
    // configure a privateKey, account, or walletClient
  } else if (e instanceof RpcError || e instanceof IndexerError) {
    // the request did not complete: retry with backoff, or degrade
  } else if (e instanceof InvariantError) {
    console.error(e); // report the SDK defect
    throw e;
  } else if (e instanceof SomniaMarketsError) {
    // NotConfiguredError and anything else the SDK raised
  } else {
    throw e; // not ours: a wallet or an application bug
  }
}

IndexerWaitTimeoutError comes from the indexer wait, not from the write. Wrap waitForIndexedBlock when you need to branch on that deadline:

ts
const targetBlock = 1_234_567n; // the confirmed transaction's block
try {
  await exchange.client.waitForIndexedBlock(exchange.client.config.chain.id, targetBlock);
} catch (e) {
  if (e instanceof IndexerWaitTimeoutError) {
    // the indexer answered but is still behind e.targetBlock
  } else if (e instanceof IndexerError) {
    // the metadata read did not complete: retry with backoff, or degrade
  } else {
    throw e;
  }
}

Message text is not stable. errorName, operation, and what are.

Decide from a revert

ContractRevertError.errorName is the contract's own error name when the revert data matched a known error. Branch on it with a fallback arm; errorName is undefined for a bare require string or unknown data.

ts
import { ContractRevertError } from "@somnia-chain/markets-sdk";

try {
  await exchange.trader.placeOrder(params);
} catch (e) {
  if (!(e instanceof ContractRevertError)) throw e;
  switch (e.errorName) {
    case "ExpiredOrderMustBeCancelled": {
      // An expired maker sits in the way. Sweep the expired orders, then retry.
      const expired = await exchange.client.listSweepableOrders({ pool: params.pool });
      await exchange.trader.cancelExpiredOrders({ pool: params.pool, orderIds: expired.map((o) => o.orderId) });
      break;
    }
    case "InsufficientBalance":
    case "ERC20InsufficientBalance":
      // top up, then retry
      break;
    case "PostOnlyWouldCross":
      // re-quote one tick away
      break;
    case undefined:
      console.error("unrecognised revert", e.reason ?? e.data);
      break;
    default:
      throw e;
  }
}

e.args holds the decoded arguments in order. IncorrectSender(sender, expected), for example, is what a cancel of an already-cancelled order returns; expected is the zero address because the order no longer exists.

Retry only what can succeed

Retry RpcError and IndexerError with backoff; the request never reached a conclusion. An IndexerWaitTimeoutError means the indexer answered but did not reach targetBlock before the deadline: wait longer, or read the required state from chain. Retry a ContractRevertError only after the state that caused it changes; the same call reverts the same way. Never retry InvalidInputError, NotConfiguredError, or SignerRequiredError; fix the code or the configuration. A CollateralOnWrongChainError means the collateral is on another configured chain: bridge the shortfall — the error carries a plan when a route exists — and retry once it lands.

A write that throws RpcError after the transaction was sent may still have landed. Before re-sending, read your open orders at chain head with exchange.client.getOwnOpenOrdersOnchain(pool, owner); see Detect when an order fills.

Degrade an indexer read

An indexer failure is never "no rows". Catch IndexerError deliberately when a page should render with stale or empty data rather than fail.

ts
import { IndexerError } from "@somnia-chain/markets-sdk";

const trades = await exchange
  .fetchTrades("SOMI/USDso", undefined, 50)
  .catch((e) => (e instanceof IndexerError ? [] : Promise.reject(e)));

Do not apply the same pattern to a chain read: a chain read throws only on real failure, and an empty fallback would hide it.

Tell cancellation apart from failure

When you pass signal in the configuration and abort it, in-flight indexer reads re-throw your abort reason, not an IndexerError. Check e.name === "AbortError" before the SDK classes.

Decode a revert you sent yourself

If you build a transaction with the exported ABIs and send it through your own client, decodeRevert(caught) turns the failure into a ContractRevertError with the same errorName decoding. It always returns an error and never throws. Call it only on failures you know are reverts; a transport error passed to it comes back without errorName.

ts
import { decodeRevert } from "@somnia-chain/markets-sdk";

try {
  await wallet.writeContract(request);
} catch (caught) {
  const revert = decodeRevert(caught);
  console.error(revert.errorName ?? revert.reason);
}

See the failure in context

Pass a debug sink to see which span failed and with what arguments. See Debug what the SDK is doing.