ShainShain/docs
Shain/Docs/Error handling
SDK

Error handling

ShainSdkError is the single shape every SDK failure resolves to. Branch on reason instead of parsing messages.

Reason taxonomy

ReasonOriginUser-facing?
HolderBalanceTooLowProgram (6001)Yes
SessionAlreadyActiveProgram (6002)Yes
SessionExpiredProgram (6003)Yes
SessionNotFoundProgram (6004)Yes
UnauthorizedProgram (6000)No — programmer error
InvalidParameterSDK guardNo — programmer error
ProgramIdMismatchSDK guardNo — config drift
RpcErrorNetworkYes — retry with backoff
TimeoutNetworkYes — retry
UnknownFallbackDig into cause

ShainSdkError.wrap

Use wrap at the boundary between your code and untrusted errors. It normalises Anchor error codes, Solana RPC failures, network errors and anything else into a typed reason.

wraptypescript
import { ShainSdkError } from "@shain/sdk";

try {
  await client.startSession(args);
} catch (raw) {
  const err = ShainSdkError.wrap(raw);
  console.log(err.reason);  // "HolderBalanceTooLow" | "RpcError" | …
  console.log(err.cause);   // the original throwable
  throw err;                // now structured for callers upstream
}

assertU64 and assertInRange

Two small guards exported from @shain/sdk for validating numeric inputs before handing them to the program. They throw a ShainSdkError with reason: "InvalidParameter".

guardstypescript
import { assertU64, assertInRange } from "@shain/sdk";

function buildTag(raw: unknown): bigint {
  const tag = typeof raw === "bigint" ? raw : BigInt(raw as number);
  assertU64(tag, "gated_action.tag");
  return tag;
}

assertInRange(duration, 60, 30 * 24 * 3600, "session_duration");

Branching example

handle.tstypescript
async function openOrReuseSession(client: ShainClient, args: StartArgs) {
  try {
    return await client.startSession(args);
  } catch (raw) {
    const err = ShainSdkError.wrap(raw);

    if (err.reason === "SessionAlreadyActive") {
      const snap = await client.snapshotSession();
      return { signature: null, reused: true, snapshot: snap };
    }
    if (err.reason === "HolderBalanceTooLow") {
      throw new UserFacingError("Top up \$SHAIN to open a session.");
    }
    if (err.reason === "RpcError" || err.reason === "Timeout") {
      return retryWithBackoff(() => client.startSession(args));
    }
    throw err;
  }
}

What not to do

  • Don't string-match on err.message. Anchor may rephrase messages between minor versions — the numeric code and the mapped reason are stable.
  • Don't swallow Unauthorized. It indicates a programmer error (wrong signer, wrong PDA) — surface it in your logs.
  • Don't retry on InvalidParameter. The input is wrong; retrying with the same input will produce the same error.