SDK
Error handling
ShainSdkError is the single shape every SDK failure resolves to. Branch on reason instead of parsing messages.
Reason taxonomy
| Reason | Origin | User-facing? |
|---|---|---|
HolderBalanceTooLow | Program (6001) | Yes |
SessionAlreadyActive | Program (6002) | Yes |
SessionExpired | Program (6003) | Yes |
SessionNotFound | Program (6004) | Yes |
Unauthorized | Program (6000) | No — programmer error |
InvalidParameter | SDK guard | No — programmer error |
ProgramIdMismatch | SDK guard | No — config drift |
RpcError | Network | Yes — retry with backoff |
Timeout | Network | Yes — retry |
Unknown | Fallback | Dig 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.
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".
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
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.