Skip to content

Error Handling

This guide covers error handling patterns in the SIP SDK, including error types, recovery strategies, debugging tips, and solutions to common issues.

All SIP errors extend the base SIPError class, which provides machine-readable error codes, debugging context, and cause preservation.

import { SIPError, ErrorCode, isSIPError } from '@sip-protocol/sdk'
try {
await sip.execute(intent, quote)
} catch (e) {
if (isSIPError(e)) {
console.log(`Error ${e.code}: ${e.message}`)
console.log('Context:', e.context)
if (e.cause) console.log('Caused by:', e.cause)
}
}

All SIP errors include:

  • code: Machine-readable error code (e.g., SIP_2001)
  • message: Human-readable description
  • context: Additional debugging information
  • cause: Original error if wrapped
  • timestamp: When the error occurred
Error ClassDescriptionUse Case
ValidationErrorInput validation failuresInvalid amounts, chains, addresses
CryptoErrorCryptographic operation failuresEncryption, decryption, commitments
ProofErrorZK proof operation failuresProof generation, verification
IntentErrorIntent lifecycle errorsExpired intents, invalid state
NetworkErrorExternal service failuresRPC calls, API requests
WalletErrorWallet interaction failuresConnection, signing, transactions
CodeDescriptionWhen It Occurs
SIP_1000Unknown errorUnexpected failures
SIP_1001Internal errorSDK internal failures
SIP_1002Not implementedFeature not available
CodeDescriptionRecovery
SIP_2000Validation failedCheck input parameters
SIP_2001Invalid inputVerify data format and type
SIP_2002Invalid chainUse supported chain ID
SIP_2003Invalid privacy levelUse: transparent, shielded, or compliant
SIP_2004Invalid amountEnsure positive bigint value
SIP_2005Invalid hex stringCheck hex format (0x prefix)
SIP_2006Invalid keyVerify key length and format
SIP_2007Invalid addressCheck address format for chain
SIP_2008Missing required fieldProvide all required parameters
SIP_2009Value out of rangeCheck min/max constraints

Example:

try {
const intent = sip.intent()
.input('invalid_chain', 'SOL', 100n) // ❌ Invalid chain
} catch (e) {
if (hasErrorCode(e, ErrorCode.INVALID_CHAIN)) {
// Retry with valid chain
const intent = sip.intent()
.input('solana', 'SOL', 100n) // ✅ Valid
}
}
CodeDescriptionRecovery
SIP_3000Crypto operation failedRetry or check inputs
SIP_3001Encryption failedVerify key and data
SIP_3002Decryption failedCheck key matches encryption key
SIP_3003Key derivation failedVerify seed/path parameters
SIP_3004Commitment failedCheck value and blinding
SIP_3005Signature failedVerify signing key
SIP_3006Invalid curve pointCheck point coordinates
SIP_3007Invalid scalarVerify scalar is in valid range
SIP_3008Invalid key sizeCheck key is 32 bytes
SIP_3009Invalid encrypted dataVerify ciphertext integrity
SIP_3010Invalid commitmentCheck commitment format

Example:

import { generateViewingKey, encryptForViewing, decryptWithViewing, CryptoError } from '@sip-protocol/sdk'
const viewingKey = generateViewingKey('/compliance/auditor')
const data = { sender: '0x...', amount: '100', timestamp: Date.now() }
try {
const encrypted = encryptForViewing(data, viewingKey)
const decrypted = decryptWithViewing(encrypted, viewingKey)
} catch (e) {
if (e instanceof CryptoError) {
console.error('Crypto operation failed:', e.operation, e.code)
// Check if ciphertext was corrupted
// Verify viewing key is correct
}
}
CodeDescriptionRecovery
SIP_4000Proof operation failedCheck inputs and retry
SIP_4001Proof generation failedVerify proof parameters
SIP_4002Proof verification failedCheck proof and public inputs
SIP_4003Proof not implementedUse mock provider or wait for implementation
SIP_4004Proof provider not readyCall provider.initialize() first
SIP_4005Invalid proof paramsVerify all required fields

Example:

import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir'
const provider = new NoirProofProvider({ wasmPath: '/noir' })
try {
// ❌ Provider not initialized
await provider.generateFundingProof(params)
} catch (e) {
if (hasErrorCode(e, ErrorCode.PROOF_PROVIDER_NOT_READY)) {
// ✅ Initialize first
await provider.initialize()
const result = await provider.generateFundingProof(params)
}
}
CodeDescriptionRecovery
SIP_5000Intent operation failedCheck intent state and retry
SIP_5001Intent expiredCreate new intent with longer TTL
SIP_5002Intent cancelledCannot recover, create new intent
SIP_5003Intent not foundVerify intent ID
SIP_5004Invalid intent stateCheck intent lifecycle
SIP_5005Proofs requiredGenerate required proofs
SIP_5006Quote expiredRequest new quote

Example:

import { isExpired } from '@sip-protocol/sdk'
const intent = await sip.intent()
.input('solana', 'SOL', 1_000_000_000n)
.output('zcash', 'ZEC', 50_000_000n)
.ttl(300) // 5 minutes
.build()
// Before execution, check expiry
if (isExpired(intent)) {
throw new IntentError('Intent expired', ErrorCode.INTENT_EXPIRED, {
context: {
expiry: intent.expiry,
now: Math.floor(Date.now() / 1000)
}
})
}
CodeDescriptionRecovery
SIP_6000Network operation failedCheck connectivity and retry
SIP_6001Network timeoutIncrease timeout or check connection
SIP_6002Network unavailableWait and retry with backoff
SIP_6003RPC errorCheck RPC endpoint and params
SIP_6004API errorCheck API credentials and quota
SIP_6005Rate limitedWait before retrying

Example:

async function executeWithRetry(intent: ShieldedIntent, quote: Quote, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await sip.execute(intent, quote)
} catch (e) {
if (e instanceof NetworkError) {
if (hasErrorCode(e, ErrorCode.RATE_LIMITED)) {
// Exponential backoff
await delay(1000 * Math.pow(2, i))
continue
}
if (hasErrorCode(e, ErrorCode.NETWORK_TIMEOUT)) {
// Just retry
continue
}
}
throw e
}
}
throw new Error('Max retries exceeded')
}
CodeValueDescriptionRecovery
SIP_7000WALLET_UNKNOWN_ERRORGeneric wallet errorCheck wallet state
SIP_7001WALLET_NOT_CONNECTEDWallet not connectedCall wallet.connect()
SIP_7002WALLET_CONNECTION_FAILEDConnection failedCheck wallet installation
SIP_7003WALLET_SIGNING_FAILEDSigning failedRetry signing
SIP_7004WALLET_TRANSACTION_FAILEDTransaction failedCheck gas and balance

The WalletError class includes additional wallet-specific codes:

CodeDescriptionRecovery
WALLET_NOT_INSTALLEDWallet extension not foundInstall wallet browser extension
WALLET_CONNECTION_REJECTEDUser rejected connectionAsk user to approve
WALLET_SIGNING_REJECTEDUser rejected signingAsk user to approve
WALLET_TRANSACTION_REJECTEDUser rejected transactionAsk user to approve
WALLET_INSUFFICIENT_FUNDSNot enough balanceAdd funds to wallet
WALLET_UNSUPPORTED_CHAINChain not supportedSwitch to supported chain
WALLET_CHAIN_SWITCH_REJECTEDUser rejected chain switchAsk user to approve
WALLET_STEALTH_NOT_SUPPORTEDStealth addresses not supportedUse different wallet
WALLET_VIEWING_KEY_NOT_SUPPORTEDViewing keys not supportedUse different wallet

Example:

import { WalletError } from '@sip-protocol/sdk'
try {
await wallet.connect()
const signature = await wallet.signMessage(message)
} catch (e) {
if (e instanceof WalletError) {
if (e.isConnectionError()) {
// Handle connection errors
console.error('Connection failed:', e.walletCode)
}
if (e.isUserRejection()) {
// User rejected - don't retry automatically
console.log('User rejected the request')
}
if (e.isPrivacyError()) {
// Privacy features not supported
console.log('Wallet does not support privacy features')
}
}
}

For transient network errors:

async function withExponentialBackoff<T>(
fn: () => Promise<T>,
maxRetries = 5,
baseDelay = 1000
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (e) {
if (i === maxRetries - 1) throw e
if (e instanceof NetworkError) {
const delay = baseDelay * Math.pow(2, i)
const jitter = Math.random() * 1000
await sleep(delay + jitter)
continue
}
throw e // Non-retryable error
}
}
throw new Error('Unreachable')
}
// Usage
const quotes = await withExponentialBackoff(() => sip.getQuotes(intent))

For proof generation failures:

import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir'
import { MockProofProvider } from '@sip-protocol/sdk'
const providers = [
new NoirProofProvider({ wasmPath: '/noir' }),
new MockProofProvider(), // Fallback for testing
]
async function generateProofWithFallback(params: FundingProofParams) {
for (const provider of providers) {
try {
await provider.initialize()
return await provider.generateFundingProof(params)
} catch (e) {
console.warn(`Provider ${provider.constructor.name} failed:`, e)
continue
}
}
throw new ProofError('All proof providers failed', ErrorCode.PROOF_GENERATION_FAILED)
}

Prevent cascading failures:

class CircuitBreaker {
private failures = 0
private lastFailTime = 0
private readonly threshold = 5
private readonly timeout = 60000 // 1 minute
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.isOpen()) {
throw new NetworkError('Circuit breaker open', ErrorCode.NETWORK_UNAVAILABLE)
}
try {
const result = await fn()
this.reset()
return result
} catch (e) {
this.recordFailure()
throw e
}
}
private isOpen(): boolean {
if (this.failures >= this.threshold) {
if (Date.now() - this.lastFailTime < this.timeout) {
return true
}
this.reset()
}
return false
}
private recordFailure() {
this.failures++
this.lastFailTime = Date.now()
}
private reset() {
this.failures = 0
}
}
const breaker = new CircuitBreaker()
const result = await breaker.execute(() => sip.execute(intent, quote))

For optional features:

import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir'
async function createIntent(withProofs = true) {
const builder = sip.intent()
.input('solana', 'SOL', 1_000_000_000n)
.output('zcash', 'ZEC', 50_000_000n)
.privacy(PrivacyLevel.SHIELDED)
if (withProofs) {
try {
// Try to generate proofs
const proofProvider = new NoirProofProvider({ wasmPath: '/noir' })
await proofProvider.initialize()
builder.withProvider(proofProvider)
} catch (e) {
console.warn('Proof generation not available, continuing without proofs:', e)
// Continue without proofs (for development)
}
}
return builder.build()
}

The SDK doesn’t include built-in debug logging by default, but you can wrap operations:

function withLogging<T>(
fn: () => Promise<T>,
operation: string
): Promise<T> {
console.log(`[DEBUG] Starting: ${operation}`)
const start = Date.now()
return fn()
.then(result => {
console.log(`[DEBUG] Success: ${operation} (${Date.now() - start}ms)`)
return result
})
.catch(e => {
console.error(`[DEBUG] Failed: ${operation} (${Date.now() - start}ms)`, e)
throw e
})
}
// Usage
const intent = await withLogging(
() => sip.intent()
.input('solana', 'SOL', 1_000_000_000n)
.output('zcash', 'ZEC', 50_000_000n)
.build(),
'intent creation'
)

All SIP errors include context for debugging:

try {
await sip.execute(intent, quote)
} catch (e) {
if (isSIPError(e)) {
console.log('Error Details:', {
code: e.code,
message: e.message,
context: e.context,
timestamp: e.timestamp,
stack: e.stack,
})
// Serialize for logging
const serialized = e.toJSON()
await logToService(serialized)
}
}

Use viewing keys to debug privacy transactions:

import { generateViewingKey, encryptForViewing, decryptWithViewing } from '@sip-protocol/sdk'
// Create auditor viewing key
const auditorKey = generateViewingKey('/compliance/auditor')
// Encrypt transaction details
const txDetails = {
sender: stealthAddress.spendingKey,
recipient: recipientAddress,
amount: '1000000000',
timestamp: Date.now(),
}
const encrypted = encryptForViewing(txDetails, auditorKey)
// Later, auditor can decrypt
try {
const decrypted = decryptWithViewing(encrypted, auditorKey)
console.log('Transaction details:', decrypted)
} catch (e) {
console.error('Failed to decrypt with viewing key:', e)
// Wrong viewing key or corrupted data
}

Use mock components to test error handling:

import { createMockSolanaAdapter } from '@sip-protocol/sdk'
// Test connection rejection
const rejectingWallet = createMockSolanaAdapter({
shouldFailConnect: true,
})
try {
await rejectingWallet.connect()
} catch (e) {
expect(e).toBeInstanceOf(WalletError)
expect(e.walletCode).toBe('WALLET_CONNECTION_FAILED')
}
// Test signing rejection
const rejectingWallet2 = createMockSolanaAdapter({
shouldFailSign: true,
})
await rejectingWallet2.connect()
try {
await rejectingWallet2.signMessage(new Uint8Array([1, 2, 3]))
} catch (e) {
expect(e).toBeInstanceOf(WalletError)
expect(e.walletCode).toBe('WALLET_SIGNING_FAILED')
}

Symptoms:

  • Error code: SIP_5000 or SIP_7004
  • Message: “Transaction failed” or “Intent execution failed”

Causes:

  1. Insufficient funds in sender wallet
  2. Slippage tolerance too low
  3. Quote expired before execution
  4. Network congestion causing timeout
  5. Invalid intent state

Solutions:

// 1. Check wallet balance before execution
const balance = await wallet.getBalance()
if (balance < intent.inputAmount) {
throw new ValidationError('Insufficient funds', 'input.amount', {
required: intent.inputAmount,
available: balance,
})
}
// 2. Increase slippage tolerance on the intent itself (1% = .slippage(1))
const intentWithSlippage = await sip.intent()
.input('solana', 'SOL', 1_000_000_000n)
.output('zcash', 'ZEC', 50_000_000n)
.slippage(1) // 1%
.build()
const quote = await sip.getQuotes(intentWithSlippage)
// 3. Check quote expiry
const now = Math.floor(Date.now() / 1000)
if (quote.expiry < now) {
// Get new quote
const newQuote = await sip.getQuotes(intent)
}
// 4. Set longer timeout
const result = await Promise.race([
sip.execute(intent, quote),
new Promise((_, reject) =>
setTimeout(() => reject(new NetworkError('Timeout', ErrorCode.NETWORK_TIMEOUT)), 60000)
)
])
// 5. Verify intent state
if (isExpired(intent)) {
throw new IntentError('Intent expired', ErrorCode.INTENT_EXPIRED)
}

Symptoms:

  • Error code: SIP_2007
  • Message: “Validation failed for ‘stealthAddress’: Invalid format”

Causes:

  1. Incorrect address format (missing prefix or invalid encoding)
  2. Wrong chain specified
  3. Corrupted keys during generation
  4. Using regular address instead of stealth address

Solutions:

import {
generateStealthMetaAddress,
encodeStealthMetaAddress,
decodeStealthMetaAddress,
} from '@sip-protocol/sdk'
// ✅ Generate valid stealth meta-address (chain is required)
const keys = generateStealthMetaAddress('solana')
const encoded = encodeStealthMetaAddress(keys.metaAddress)
// Validate format: sip:solana:<spendingKey>:<viewingKey>
if (!encoded.startsWith('sip:solana:')) {
throw new ValidationError('Invalid stealth address format')
}
// ✅ Decode safely — decodeStealthMetaAddress validates the keys and
// throws a ValidationError if the format or curve is wrong
try {
const decoded = decodeStealthMetaAddress(encoded)
console.log('Valid stealth address:', decoded)
} catch (e) {
throw new ValidationError('Invalid stealth address for chain', 'stealthAddress')
}

Symptoms:

  • Error code: SIP_4002
  • Message: “Proof verification failed”

Causes:

  1. Proof generated with wrong parameters
  2. Public inputs don’t match private inputs
  3. Proof provider not initialized
  4. Circuit mismatch (wrong version)

Troubleshooting:

import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir'
import { commit, generateRandomBytes } from '@sip-protocol/sdk'
const provider = new NoirProofProvider({ wasmPath: '/noir' })
// 1. Always initialize first
await provider.initialize()
// 2. Generate proof with correct parameters
const balance = 1000n
const blindingFactor = generateRandomBytes(32) // 32-byte Uint8Array
const proofResult = await provider.generateFundingProof({
balance,
minimumRequired: 100n,
blindingFactor, // Use same blinding as commitment
assetId: 'SOL',
userAddress: wallet.address,
ownershipSignature, // Signature over userAddress
})
// 3. Verify proof with matching public inputs
const publicInputs = {
commitment: commit(balance, blindingFactor).commitment, // Must match!
minimumRequired: 100n,
assetId: 'SOL',
}
const isValid = await provider.verifyFundingProof(
proofResult.proof,
publicInputs
)
if (!isValid) {
// Debug: Check each public input
console.log('Public inputs:', publicInputs)
console.log('Proof data:', proofResult)
// Likely cause: Commitment doesn't match balance+blinding
}

Symptoms:

  • Error code: SIP_7001
  • Message: “Wallet not connected. Call connect() first.”

Causes:

  1. Forgot to call wallet.connect()
  2. User rejected connection
  3. Wallet extension not installed
  4. Wallet disconnected after initial connection

Solutions:

import { WalletError } from '@sip-protocol/sdk'
async function ensureConnected(wallet: WalletAdapter) {
if (wallet.isConnected()) {
return // Already connected
}
try {
await wallet.connect()
} catch (e) {
if (e instanceof WalletError) {
switch (e.walletCode) {
case 'WALLET_NOT_INSTALLED':
throw new Error('Please install a compatible wallet extension')
case 'WALLET_CONNECTION_REJECTED':
throw new Error('Please approve the wallet connection request')
case 'WALLET_CONNECTION_FAILED':
// Retry once
await wallet.connect()
break
default:
throw e
}
}
throw e
}
}
// Usage
await ensureConnected(wallet)
const signature = await wallet.signMessage(message)

Symptoms:

  • Error code: SIP_3010
  • Message: “Invalid commitment” or “Commitment verification failed”

Causes:

  1. Using different blinding factor for verification
  2. Homomorphic addition done incorrectly
  3. Amount overflow (exceeds curve order)
  4. Corrupted commitment data

Solutions:

import { commit, verifyOpening, addCommitments, addBlindings } from '@sip-protocol/sdk'
// ✅ Store blinding factor with commitment
const amount = 1000n
const { commitment, blinding } = commit(amount)
// Save both for later verification
const stored = {
commitment,
blinding: blinding, // Must save this!
amount,
}
// ✅ Verify with saved blinding
const isValid = verifyOpening(
stored.commitment,
stored.amount,
stored.blinding
)
console.assert(isValid, 'Verification should succeed')
// ✅ Homomorphic addition
const c1 = commit(100n)
const c2 = commit(200n)
const sum = addCommitments(c1.commitment, c2.commitment)
// Verify sum (must use sum of blindings — addBlindings sums them mod the curve order)
const totalBlinding = addBlindings(c1.blinding, c2.blinding)
verifyOpening(sum.commitment, 300n, totalBlinding) // ✅

Convert unknown errors to SIPError:

import { wrapError, ErrorCode } from '@sip-protocol/sdk'
async function riskyOperation() {
try {
await externalLibrary.doSomething()
} catch (e) {
throw wrapError(
e,
'External operation failed',
ErrorCode.INTERNAL,
{ operation: 'doSomething' }
)
}
}

Check error types safely:

import { isSIPError, hasErrorCode, ErrorCode } from '@sip-protocol/sdk'
try {
await sip.execute(intent, quote)
} catch (e) {
if (isSIPError(e)) {
// TypeScript knows e is SIPError
console.log(e.code, e.context)
}
if (hasErrorCode(e, ErrorCode.INTENT_EXPIRED)) {
// Handle specific error code
console.log('Intent expired, creating new one')
}
}

Safely extract message from unknown errors:

import { getErrorMessage } from '@sip-protocol/sdk'
try {
await operation()
} catch (e) {
// Works for Error, SIPError, or any unknown type
const message = getErrorMessage(e)
console.error('Operation failed:', message)
}
  1. Always check error types before handling - use instanceof or hasErrorCode()
  2. Preserve error causes - use the cause option when wrapping errors
  3. Add context - include relevant debugging information in context field
  4. Don’t retry user rejections - only retry transient errors (network, timeout)
  5. Use circuit breakers - prevent cascading failures to external services
  6. Log serialized errors - use error.toJSON() for structured logging
  7. Test error paths - use mock adapters to test error handling
  8. Provide recovery steps - give users actionable error messages
  9. Validate early - check inputs before expensive operations
  10. Use graceful degradation - fall back to safe defaults when possible