SAP Explorer Docs
Best Practices

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:

CategoryCauseRecoveryHow to Identify
RPC errorsNetwork timeout, 429 rate limit, node unavailableRetry with backoffError message contains "429", "timeout", or "ECONNREFUSED"
Anchor errorsConstraint violation, insufficient funds, wrong signerFix inputs, check stateError contains hex code like "0x1770"
Account errorsPDA not found, account not initializedInitialize first, check derivationError contains "Account does not exist"
SDK errorsInvalid parameters, type mismatchFix calling codeTypeScript compile error or runtime type check
Simulation errorsTX would fail (preflight check)Inspect logs, fix instructionError 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 permission
import {
  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 CodeDecimalMeaning
0x00Account already initialized
0x11Insufficient lamports
0x7d32003Constraint violation (has_one)
0xbbf3007Account not initialized
0xbc23010Account 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):

CodeNameDescription
6000AgentAlreadyRegisteredThis wallet already has a registered agent
6001AgentNotFoundNo agent PDA exists for this wallet
6009EscrowExpiredEscrow past its expiresAt timestamp
6010EscrowInsufficientBalanceBalance too low for settlement
6015VaultAlreadyInitializedVault with this nonce already exists
6017SessionClosedSession is closed, no further writes
6018DataExceedsMaxWriteSizeData exceeds 750-byte limit
6019RingBufferOverflowRing 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;
}