Error Handling
Structured error handling patterns for SAP SDK calls, Anchor errors, and API routes.
Error Handling
Robust error handling is critical for on-chain applications. Unlike traditional APIs where errors are usually recoverable with a simple retry, blockchain operations can fail for reasons that require different recovery strategies: insufficient funds, expired accounts, rate limits from RPC nodes, or constraint violations in the smart contract.
This guide covers the SAP SDK error hierarchy, Anchor program errors, API route patterns, and recovery strategies. Understanding these patterns will save significant debugging time.
Error Categories
SAP operations can fail for several distinct reasons. Identifying the category quickly determines the right recovery strategy:
| Category | Cause | Recovery | How to Identify |
|---|---|---|---|
| RPC errors | Network timeout, 429 rate limit, node unavailable | Retry with backoff | Error message contains "429", "timeout", or "ECONNREFUSED" |
| Anchor errors | Constraint violation, insufficient funds, wrong signer | Fix inputs, check state | Error contains hex code like "0x1770" |
| Account errors | PDA not found, account not initialized | Initialize first, check derivation | Error contains "Account does not exist" |
| SDK errors | Invalid parameters, type mismatch | Fix calling code | TypeScript compile error or runtime type check |
| Simulation errors | TX would fail (preflight check) | Inspect logs, fix instruction | Error contains "Simulation failed" |
SDK Error Hierarchy
The SDK provides a structured error hierarchy so you can handle each failure mode differently. All SDK errors extend SapError:
SapError (base)
├── SapValidationError — invalid inputs, length limits, type mismatches
├── SapRpcError — network timeouts, 429 rate limits, node failures
├── SapAccountNotFoundError — PDA doesn't exist, account not initialized
├── SapTimeoutError — operation exceeded timeout threshold
└── SapPermissionError — wrong signer, delegate lacks permissionimport {
SapError,
SapValidationError,
SapRpcError,
SapAccountNotFoundError,
SapTimeoutError,
SapPermissionError,
} from "@oobe-protocol-labs/synapse-sap-sdk";
try {
await client.builder
.agent("MyAgent")
.description("Service agent")
.register();
} catch (error: unknown) {
if (error instanceof SapAccountNotFoundError) {
// PDA doesn't exist yet — expected on first run
} else if (error instanceof SapValidationError) {
// Bad inputs — fix calling code
console.error("Validation:", error.message);
} else if (error instanceof SapRpcError) {
// Network issue — retry with backoff
console.error("RPC:", error.message);
} else if (error instanceof SapTimeoutError) {
// Timed out — retry or increase timeout
console.error("Timeout:", error.message);
} else if (error instanceof SapPermissionError) {
// Wrong signer or delegate lacks permission
console.error("Permission:", error.message);
} else if (error instanceof SapError) {
// Other SDK error
console.error("SAP:", error.message);
} else {
console.error("Unexpected:", error);
}
}classifyAnchorError and extractAnchorErrorCode
The SDK provides two utilities that convert raw Anchor error messages into structured information:
import {
classifyAnchorError,
extractAnchorErrorCode,
} from "@oobe-protocol-labs/synapse-sap-sdk";
try {
await client.agent.register(/* ... */);
} catch (error: unknown) {
if (error instanceof Error) {
// Extract the numeric error code from the message
const code = extractAnchorErrorCode(error);
// code → 6000 (number) or null if not an Anchor error
// Classify into a structured object
const classified = classifyAnchorError(error);
// classified → { code: 6000, name: "AgentAlreadyRegistered", ... }
// or null if the error isn't from Anchor
if (code === 6000) {
console.log("Agent already registered — skipping.");
}
}
}Anchor Error Code Reference
Common Anchor framework errors encountered in SAP:
| Hex Code | Decimal | Meaning |
|---|---|---|
0x0 | 0 | Account already initialized |
0x1 | 1 | Insufficient lamports |
0x7d3 | 2003 | Constraint violation (has_one) |
0xbbf | 3007 | Account not initialized |
0xbc2 | 3010 | Account owned by wrong program |
0x1770+ | 6000+ | Custom program errors (SAP-specific) |
SAP Program Error Codes
These are the custom errors defined by the SAP program (offset from 6000):
| Code | Name | Description |
|---|---|---|
| 6000 | AgentAlreadyRegistered | This wallet already has a registered agent |
| 6001 | AgentNotFound | No agent PDA exists for this wallet |
| 6009 | EscrowExpired | Escrow past its expiresAt timestamp |
| 6010 | EscrowInsufficientBalance | Balance too low for settlement |
| 6015 | VaultAlreadyInitialized | Vault with this nonce already exists |
| 6017 | SessionClosed | Session is closed, no further writes |
| 6018 | DataExceedsMaxWriteSize | Data exceeds 750-byte limit |
| 6019 | RingBufferOverflow | Ring buffer full, seal page first |
See the Troubleshooting guide for detailed fixes for each error.
API Route Error Pattern
Wrap all SDK calls in API routes with structured error responses:
// src/app/api/sap/agent/route.ts
import { NextResponse } from "next/server";
export async function GET() {
try {
const agent = await client.agent.fetch();
return NextResponse.json({ data: agent });
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : "Unknown error";
// Classify the error for the client
if (message.includes("Account does not exist")) {
return NextResponse.json(
{ error: "Agent not found", code: "NOT_FOUND" },
{ status: 404 },
);
}
if (message.includes("429")) {
return NextResponse.json(
{ error: "Rate limited", code: "RATE_LIMITED" },
{ status: 429 },
);
}
console.error("[API] Agent fetch failed:", message);
return NextResponse.json(
{ error: "Internal error", code: "INTERNAL" },
{ status: 500 },
);
}
}Client-Side Hook Pattern
export function useAgent() {
const [data, setData] = useState<Agent | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/sap/agent")
.then(async (res) => {
if (!res.ok) {
const body = await res.json();
throw new Error(body.error || `HTTP ${res.status}`);
}
return res.json();
})
.then((json) => setData(json.data))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);
return { data, error, loading };
}Transaction Simulation
Before sending a transaction, simulate it to catch errors early:
try {
const result = await client.agent.register(params);
} catch (error: unknown) {
if (
error instanceof Error &&
error.message.includes("Simulation failed")
) {
// Extract program logs from the error
const logs = (error as any).logs as string[] | undefined;
if (logs) {
console.error("Program logs:");
for (const log of logs) {
console.error(" ", log);
}
}
}
}Recovery Patterns
Idempotent Operations
Many SAP operations are safe to retry. Prefer idempotent patterns:
// Safe: start() checks if session exists before creating
await client.session.start("session-id");
// Safe: fetch returns null if not initialized
const agent = await client.agent.fetchNullable();Graceful Degradation
When reads fail, show cached data rather than an error screen:
const cached = sessionStorage.getItem("agent-data");
try {
const fresh = await fetch("/api/sap/agent").then((r) => r.json());
sessionStorage.setItem("agent-data", JSON.stringify(fresh));
return fresh;
} catch {
return cached ? JSON.parse(cached) : null;
}