Error Handling
Structured error handling patterns for SAP SDK v0.20.0 calls, Anchor errors, API routes, and recovery strategies.
Error Handling
SDK Version: v0.20.0
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 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 (v0.20.0)
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 permission
├── SapInstructionError — on-chain instruction failed (v0.20.0)
└── SapDisputeError — dispute-related errors (v0.20.0)Example: Structured Error Handling
import {
SapError,
SapValidationError,
SapRpcError,
SapAccountNotFoundError,
SapTimeoutError,
SapPermissionError,
SapInstructionError,
} 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
console.log("Agent not found - needs registration");
} 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 SapInstructionError) {
// On-chain instruction failed - check program logs
console.error("Instruction:", error.code, error.logs);
} 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 (v0.20.0)
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 |
| 6002 | InvalidCapabilityFormat | Capability JSON is malformed |
| 6003 | MaxCapabilitiesExceeded | More than 32 capabilities |
| 6009 | EscrowExpired | Escrow past its expiresAt timestamp |
| 6010 | EscrowInsufficientBalance | Balance too low for settlement |
| 6011 | EscrowNotActive | Escrow is closed or disputed |
| 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 |
| 6020 | InvalidEpochIndex | Epoch index doesn't match sequence / 1000 |
| 6021 | DelegateExpired | Vault delegate has expired |
| 6022 | DisputeAlreadyFiled | Dispute already exists for this escrow |
| 6023 | DisputeWindowExpired | Dispute filing window has passed |
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 }
);
}
if (message.includes("Simulation failed")) {
return NextResponse.json(
{ error: "Transaction simulation failed", code: "SIMULATION_FAILED" },
{ status: 400 }
);
}
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;
}Retry with Exponential Backoff
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 500,
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: unknown) {
if (attempt === maxRetries) throw error;
const isRateLimit =
error instanceof Error && error.message.includes("429");
const delay = isRateLimit
? baseDelay * Math.pow(2, attempt)
: baseDelay;
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("Unreachable");
}Error Monitoring
For production applications, implement error monitoring:
// src/lib/error-monitoring.ts
import { SapError } from "@oobe-protocol-labs/synapse-sap-sdk";
export function reportError(error: unknown, context: string) {
if (error instanceof SapError) {
// Send to monitoring service
console.error(`[SAP:${context}]`, {
type: error.constructor.name,
code: (error as any).code,
message: error.message,
timestamp: Date.now(),
});
} else {
console.error(`[Unknown:${context}]`, error);
}
}
// Usage
try {
await client.agent.register(params);
} catch (error: unknown) {
reportError(error, "agent.register");
throw error; // Re-throw for caller to handle
}Next Steps
- Troubleshooting — Common errors and fixes
- RPC Configuration — Reliable connectivity
- Security — Key management best practices
Last Updated: June 2026
SDK Version: 0.20.0