Skip to main content
All SDK failures are thrown as VaultError or its subclasses, providing consistent error handling across all payment providers.

Error Shape

Every VaultError includes comprehensive error information:
VaultError Structure
class VaultError extends Error {
  readonly code: string;           // Machine-readable error code
  readonly category: string;       // Error category
  readonly suggestion: string;     // Actionable suggestion
  readonly docsUrl: string;        // Documentation URL
  readonly retriable: boolean;     // Whether retry is recommended
  readonly context: VaultErrorContext; // Error context
}

Error Fields

FieldTypeDescription
codestringMachine-readable error code (e.g., CARD_DECLINED)
categoryVaultErrorCategoryError category for grouping
suggestionstringHuman-readable suggestion for resolution
docsUrlstringLink to relevant documentation
retriablebooleanWhether the operation can be safely retried
contextVaultErrorContextAdditional context (provider, operation, etc.)

Context Object

src/errors/vault-error.ts:7
export interface VaultErrorContext {
  provider?: string;
  operation?: string;
  requestId?: string;
  providerCode?: string;
  providerMessage?: string;
  [key: string]: unknown;
}

Error Subclasses

The SDK provides specialized error classes for different failure scenarios:

VaultConfigError

Thrown when configuration is invalid:
import { VaultConfigError } from '@vaultsaas/core';

try {
  const vault = new VaultClient({
    providers: {},  // No providers!
    routing: { rules: [] },
  });
} catch (error) {
  if (error instanceof VaultConfigError) {
    console.error(error.code); // "INVALID_CONFIGURATION"
    console.error(error.suggestion);
  }
}

VaultRoutingError

Thrown when routing fails:
import { VaultRoutingError } from '@vaultsaas/core';

try {
  await vault.charge({
    amount: 2500,
    currency: 'USD',
    paymentMethod: { type: 'card', token: 'pm_card_visa' },
    routing: {
      provider: 'nonexistent',
    },
  });
} catch (error) {
  if (error instanceof VaultRoutingError) {
    console.error(error.code); // "ROUTING_PROVIDER_UNAVAILABLE"
    console.error(error.context.provider); // "nonexistent"
  }
}

VaultProviderError

Thrown when provider API calls fail:
import { VaultProviderError } from '@vaultsaas/core';

try {
  await vault.charge({
    amount: 2500,
    currency: 'USD',
    paymentMethod: {
      type: 'card',
      number: '4000000000000002', // Stripe test card for decline
      expMonth: 12,
      expYear: 2030,
      cvc: '123',
    },
  });
} catch (error) {
  if (error instanceof VaultProviderError) {
    console.error(error.code); // "CARD_DECLINED"
    console.error(error.context.provider); // "stripe"
    console.error(error.context.providerCode); // "card_declined"
  }
}

VaultNetworkError

Thrown when network failures occur:
import { VaultNetworkError } from '@vaultsaas/core';

try {
  await vault.charge(request);
} catch (error) {
  if (error instanceof VaultNetworkError) {
    console.error(error.code); // "NETWORK_ERROR"
    console.error(error.retriable); // true
    
    if (error.retriable) {
      // Queue for retry with exponential backoff
    }
  }
}

WebhookVerificationError

Thrown when webhook signature verification fails:
import { WebhookVerificationError } from '@vaultsaas/core';

try {
  const event = await vault.handleWebhook(
    'stripe',
    payload,
    { 'stripe-signature': 'invalid' }
  );
} catch (error) {
  if (error instanceof WebhookVerificationError) {
    console.error(error.code); // "WEBHOOK_SIGNATURE_INVALID"
    // Return 400 to provider
  }
}

VaultIdempotencyConflictError

Thrown when idempotency key is reused with different payload:
import { VaultIdempotencyConflictError } from '@vaultsaas/core';

try {
  await vault.charge({
    amount: 3000, // Different from original
    currency: 'USD',
    paymentMethod: { type: 'card', token: 'pm_card_visa' },
    idempotencyKey: 'order-1001', // Same key
  });
} catch (error) {
  if (error instanceof VaultIdempotencyConflictError) {
    console.error(error.code); // "IDEMPOTENCY_CONFLICT"
    console.error(error.suggestion);
    // "Use a unique idempotency key for different request payloads"
  }
}

Complete Error Handling Example

import { StripeAdapter, VaultClient, VaultError } from '@vaultsaas/core';

function mustEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing env var: ${name}`);
  return value;
}

const vault = new VaultClient({
  providers: {
    stripe: {
      adapter: StripeAdapter,
      config: { apiKey: mustEnv('STRIPE_API_KEY') },
    },
  },
  routing: {
    rules: [{ match: { default: true }, provider: 'stripe' }],
  },
});

try {
  await vault.charge({
    amount: 2500,
    currency: 'USD',
    paymentMethod: {
      type: 'card',
      number: '4000000000000002', // Common Stripe decline test card
      expMonth: 12,
      expYear: 2030,
      cvc: '123',
    },
    customer: {
      email: '[email protected]',
    },
  });
} catch (error) {
  if (error instanceof VaultError) {
    console.error('Vault error', {
      code: error.code,
      category: error.category,
      retriable: error.retriable,
      suggestion: error.suggestion,
      docsUrl: error.docsUrl,
      context: error.context,
    });

    if (error.retriable) {
      // Queue retry with exponential backoff
    } else {
      // Show actionable failure to caller
    }
  } else {
    throw error;
  }
}

Error Categories

Errors are grouped into categories for easier handling:
type VaultErrorCategory =
  | 'configuration'
  | 'validation'
  | 'routing'
  | 'authentication'
  | 'authorization'
  | 'card_error'
  | 'network_error'
  | 'provider_error'
  | 'rate_limit'
  | 'idempotency'
  | 'unknown';

Provider Error Mapping

The SDK normalizes provider-specific errors into canonical codes:
src/client/vault-client.ts:636
private async wrapProviderCall<T>(
  provider: string,
  operation: string,
  execute: () => Promise<T>,
): Promise<T> {
  try {
    return await execute();
  } catch (error) {
    if (error instanceof VaultError) {
      throw error;
    }

    throw mapProviderError(error, {
      provider,
      operation,
    });
  }
}
Provider-specific error codes and messages are preserved in context.providerCode and context.providerMessage for debugging.
Log essential error information:
if (error instanceof VaultError) {
  logger.error('Payment operation failed', {
    code: error.code,
    category: error.category,
    provider: error.context.provider,
    operation: error.context.operation,
    retriable: error.retriable,
  });
}

Best Practices

Always check retriable before retryingRetrying non-retriable errors wastes resources and may make things worse.
Log provider context for debugging
logger.error('Payment failed', {
  provider: error.context.provider,
  providerCode: error.context.providerCode,
  providerMessage: error.context.providerMessage,
});
Use error categories for groupingHandle similar errors together instead of checking individual codes.
Never expose provider error messages to usersUse error.suggestion or custom user-friendly messages instead.
Monitor high-frequency error codesTrack error rates by provider and add routing fallbacks where needed.

Next Steps

Architecture

Learn about error mapping

Idempotency

Handle idempotency conflicts

Build docs developers (and LLMs) love