Common Errors & Solutions

This guide provides detailed information about common error codes you might encounter when using the Unified Commerce API, along with solutions and code examples.

Error Response Format

All API errors follow a consistent format:

{
  "errors": [
    {
      "message": "Authentication failed",
      "extensions": {
        "code": "UNAUTHENTICATED",
        "statusCode": 401,
        "requestId": "req_abc123def456",
        "timestamp": "2024-01-15T10:30:00Z",
        "details": {
          "field": "Authorization",
          "reason": "Invalid API key format"
        }
      }
    }
  ]
}

Authentication Errors (4xx)

UC_AUTH_001: Invalid API Key

Error Message: "The provided API key is invalid or malformed"

Status Code: 401

Common Causes:

  • Incorrect API key
  • Missing 'Bearer' prefix
  • Using test key in production
  • Expired or revoked key

Solution:

// ❌ Wrong - Common mistakes
const headers = {
  'Authorization': 'uc_live_sk_abc123'  // Missing Bearer
  'Authorization': 'Bearer  uc_live_sk_abc123'  // Extra space
  'Authorization': '"Bearer uc_live_sk_abc123"'  // Extra quotes
  'Authorization': 'Bearer uc_test_sk_abc123'  // Test key in production
};

// ✅ Correct
const headers = {
  'Authorization': 'Bearer uc_live_sk_abc123'
};

// Better - Use environment variables
const headers = {
  'Authorization': `Bearer ${process.env.UC_API_KEY}`
};

Debugging:

# Test your API key
curl -X POST https://gateway.unifiedcommerce.app/graphql \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ viewer { id } }"}'

UC_AUTH_002: Expired Token

Error Message: "Authentication token has expired"

Status Code: 401

Common Causes:

  • JWT token expired
  • Session timeout
  • Clock skew between client and server

Solution:

class TokenManager {
  constructor() {
    this.token = null;
    this.expiresAt = null;
  }

  async getToken() {
    // Check if token is still valid
    if (this.token && this.expiresAt > Date.now()) {
      return this.token;
    }

    // Refresh token
    return await this.refreshToken();
  }

  async refreshToken() {
    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          refreshToken: localStorage.getItem('refreshToken')
        })
      });

      const data = await response.json();
      this.token = data.accessToken;
      this.expiresAt = Date.now() + (data.expiresIn * 1000);

      return this.token;
    } catch (error) {
      // Handle refresh failure
      window.location.href = '/login';
      throw error;
    }
  }
}

// Usage with Apollo Client
const tokenManager = new TokenManager();

const authLink = setContext(async (_, { headers }) => {
  const token = await tokenManager.getToken();
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    }
  };
});

UC_AUTH_003: Insufficient Permissions

Error Message: "You do not have permission to perform this action"

Status Code: 403

Common Causes:

  • API key lacks required scopes
  • User role insufficient
  • Resource belongs to different merchant
  • Feature not available in current plan

Solution:

// Check permissions before making request
const CHECK_PERMISSIONS = gql`
  query CheckPermissions {
    viewer {
      id
      permissions
      role
      merchant {
        id
        plan
        features
      }
    }
  }
`;

async function canPerformAction(action) {
  const { data } = await client.query({
    query: CHECK_PERMISSIONS
  });

  const permissions = data.viewer.permissions;
  return permissions.includes(action);
}

// Use permission check
async function deleteProduct(productId) {
  // Check permission first
  if (!await canPerformAction('products:delete')) {
    throw new Error('Insufficient permissions to delete products');
  }

  // Proceed with deletion
  return await client.mutate({
    mutation: DELETE_PRODUCT,
    variables: { id: productId }
  });
}

// Handle permission errors gracefully
try {
  await deleteProduct('prod_123');
} catch (error) {
  if (error.extensions?.code === 'FORBIDDEN') {
    // Show user-friendly message
    showNotification('You need admin privileges to delete products');
    // Optionally request permission upgrade
    requestPermissionUpgrade(['products:delete']);
  }
}

UC_AUTH_004: IP Address Not Whitelisted

Error Message: "Request from IP address not in whitelist"

Status Code: 403

Common Causes:

  • IP whitelisting enabled but current IP not added
  • Dynamic IP address changed
  • Request from unexpected location

Solution:

// Add IP to whitelist via API
const ADD_IP_WHITELIST = gql`
  mutation AddIPToWhitelist($ip: String!, $description: String) {
    addIPWhitelist(ip: $ip, description: $description) {
      success
      ipAddress
    }
  }
`;

// For development - use CIDR ranges
await client.mutate({
  mutation: ADD_IP_WHITELIST,
  variables: {
    ip: '192.168.1.0/24',  // Entire subnet
    description: 'Office network'
  }
});

// For production - be specific
await client.mutate({
  mutation: ADD_IP_WHITELIST,
  variables: {
    ip: '203.0.113.1',
    description: 'Production server'
  }
});

Validation Errors (4xx)

UC_VAL_001: Missing Required Field

Error Message: "Field [fieldName] is required but was not provided"

Status Code: 400

Common Causes:

  • Required field not included in mutation
  • Field is null when non-nullable
  • Empty string when field requires value

Solution:

// Use validation library before sending
import * as yup from 'yup';

const productSchema = yup.object().shape({
  name: yup.string().required('Product name is required'),
  price: yup.number().required('Price is required').positive(),
  sku: yup.string().required('SKU is required'),
  description: yup.string().nullable(),
  categoryId: yup.string().required('Category is required')
});

async function createProduct(input) {
  try {
    // Validate input first
    const validatedInput = await productSchema.validate(input, {
      abortEarly: false  // Get all validation errors
    });

    // Send validated input
    return await client.mutate({
      mutation: CREATE_PRODUCT,
      variables: { input: validatedInput }
    });
  } catch (validationError) {
    if (validationError.inner) {
      // Yup validation error
      const errors = validationError.inner.map(err => ({
        field: err.path,
        message: err.message
      }));
      console.error('Validation errors:', errors);
      throw new ValidationError(errors);
    }
    throw validationError;
  }
}

// TypeScript for compile-time checking
interface ProductInput {
  name: string;
  price: number;
  sku: string;
  description?: string | null;
  categoryId: string;
}

function createProductTyped(input: ProductInput) {
  // TypeScript ensures required fields
  return client.mutate({
    mutation: CREATE_PRODUCT,
    variables: { input }
  });
}

UC_VAL_002: Invalid Field Format

Error Message: "Field [fieldName] has invalid format. Expected: [format]"

Status Code: 400

Common Causes:

  • Email not in valid format
  • Phone number incorrect format
  • Date/time format mismatch
  • Invalid enum value

Solution:

// Format validators
const formatters = {
  email: (email) => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!regex.test(email)) {
      throw new Error('Invalid email format');
    }
    return email.toLowerCase().trim();
  },

  phone: (phone) => {
    // Remove all non-digits
    const cleaned = phone.replace(/\D/g, '');

    // Format as needed
    if (cleaned.length === 10) {
      return `+1${cleaned}`;  // US number
    }

    if (!cleaned.startsWith('1') && cleaned.length === 11) {
      return `+${cleaned}`;
    }

    throw new Error('Invalid phone number');
  },

  date: (date) => {
    // Ensure ISO 8601 format
    if (date instanceof Date) {
      return date.toISOString();
    }

    // Parse and validate
    const parsed = new Date(date);
    if (isNaN(parsed.getTime())) {
      throw new Error('Invalid date format');
    }

    return parsed.toISOString();
  },

  currency: (amount) => {
    // Ensure cents (integer)
    if (typeof amount === 'number') {
      return Math.round(amount * 100);
    }

    // Parse string amounts
    const cleaned = amount.replace(/[^0-9.-]/g, '');
    return Math.round(parseFloat(cleaned) * 100);
  }
};

// Use formatters before sending
const CREATE_CUSTOMER = gql`
  mutation CreateCustomer($input: CustomerInput!) {
    createCustomer(input: $input) {
      id
      email
      phone
    }
  }
`;

async function createCustomer(input) {
  // Format fields
  const formattedInput = {
    ...input,
    email: formatters.email(input.email),
    phone: formatters.phone(input.phone),
    birthDate: input.birthDate ? formatters.date(input.birthDate) : null
  };

  return await client.mutate({
    mutation: CREATE_CUSTOMER,
    variables: { input: formattedInput }
  });
}

UC_VAL_003: Value Out of Range

Error Message: "Value [value] is outside acceptable range [min-max]"

Status Code: 400

Common Causes:

  • Negative values where positive required
  • Exceeding maximum string length
  • Quantity exceeds inventory
  • Page size too large

Solution:

// Range validators with constraints
const constraints = {
  product: {
    name: { min: 3, max: 200 },
    description: { max: 5000 },
    price: { min: 0, max: 1000000 },
    quantity: { min: 0, max: 999999 },
    weight: { min: 0, max: 100000 }
  },
  pagination: {
    limit: { min: 1, max: 100 },
    offset: { min: 0 }
  },
  search: {
    query: { min: 2, max: 100 }
  }
};

function validateRange(value, field, entity = 'product') {
  const constraint = constraints[entity]?.[field];

  if (!constraint) {
    return value;  // No constraint defined
  }

  if (typeof value === 'string') {
    if (value.length < constraint.min) {
      throw new Error(
        `${field} must be at least ${constraint.min} characters`
      );
    }
    if (value.length > constraint.max) {
      throw new Error(
        `${field} must not exceed ${constraint.max} characters`
      );
    }
  } else if (typeof value === 'number') {
    if (constraint.min !== undefined && value < constraint.min) {
      throw new Error(
        `${field} must be at least ${constraint.min}`
      );
    }
    if (constraint.max !== undefined && value > constraint.max) {
      throw new Error(
        `${field} must not exceed ${constraint.max}`
      );
    }
  }

  return value;
}

// Pagination helper with automatic limiting
class PaginationHelper {
  constructor(maxLimit = 100) {
    this.maxLimit = maxLimit;
  }

  async fetchPage(query, variables) {
    // Ensure limit is within bounds
    const limit = Math.min(
      variables.limit || 20,
      this.maxLimit
    );

    return await client.query({
      query,
      variables: {
        ...variables,
        limit
      }
    });
  }

  async *fetchAll(query, variables) {
    let cursor = null;
    let hasMore = true;

    while (hasMore) {
      const { data } = await this.fetchPage(query, {
        ...variables,
        cursor
      });

      yield data.items;

      cursor = data.pageInfo.endCursor;
      hasMore = data.pageInfo.hasNextPage;
    }
  }
}

UC_VAL_004: Duplicate Entry

Error Message: "A record with [field]=[value] already exists"

Status Code: 409

Common Causes:

  • SKU already exists
  • Email already registered
  • Duplicate order number
  • Unique constraint violation

Solution:

// Check for duplicates before creating
async function createProductSafely(input) {
  // Check if SKU exists
  const existing = await client.query({
    query: gql`
      query CheckSKU($sku: String!) {
        product(sku: $sku) {
          id
          sku
        }
      }
    `,
    variables: { sku: input.sku }
  });

  if (existing.data.product) {
    // Handle duplicate - could update instead
    const shouldUpdate = await confirmDialog(
      `Product with SKU ${input.sku} exists. Update it instead?`
    );

    if (shouldUpdate) {
      return updateProduct(existing.data.product.id, input);
    }

    // Generate new SKU
    input.sku = `${input.sku}-${Date.now()}`;
  }

  // Create new product
  return await client.mutate({
    mutation: CREATE_PRODUCT,
    variables: { input }
  });
}

// Retry with unique value generator
async function createWithRetry(createFn, generateUniqueFn, maxRetries = 3) {
  let attempts = 0;
  let lastError;

  while (attempts < maxRetries) {
    try {
      return await createFn();
    } catch (error) {
      if (error.extensions?.code === 'DUPLICATE_ENTRY') {
        attempts++;
        lastError = error;

        // Generate new unique value
        await generateUniqueFn();

        // Add exponential backoff
        await new Promise(resolve =>
          setTimeout(resolve, Math.pow(2, attempts) * 100)
        );
      } else {
        throw error;  // Not a duplicate error
      }
    }
  }

  throw lastError;
}

// Usage
const product = await createWithRetry(
  () => createProduct(input),
  () => { input.sku = `${input.sku}-${crypto.randomUUID().slice(0, 8)}`; },
  3
);

Rate Limiting Errors

UC_RATE_001: Rate Limit Exceeded

Error Message: "API rate limit exceeded. Please retry after [time]"

Status Code: 429

Common Causes:

  • Too many requests in short time
  • Burst limit exceeded
  • Plan limits reached
  • No backoff implementation

Solution:

// Exponential backoff with jitter
class RateLimitHandler {
  constructor(options = {}) {
    this.maxRetries = options.maxRetries || 5;
    this.baseDelay = options.baseDelay || 1000;
    this.maxDelay = options.maxDelay || 30000;
    this.jitterRange = options.jitterRange || 1000;
  }

  async execute(fn) {
    let lastError;

    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        return await fn();
      } catch (error) {
        if (error.statusCode === 429) {
          lastError = error;

          // Get retry delay from header or calculate
          const retryAfter = this.getRetryDelay(error, attempt);

          console.log(`Rate limited. Retry ${attempt + 1}/${this.maxRetries} in ${retryAfter}ms`);

          await this.delay(retryAfter);
        } else {
          throw error;  // Not a rate limit error
        }
      }
    }

    throw lastError;
  }

  getRetryDelay(error, attempt) {
    // Check for Retry-After header
    if (error.headers?.['retry-after']) {
      const retryAfter = error.headers['retry-after'];

      // Header can be seconds or HTTP date
      if (isNaN(retryAfter)) {
        return new Date(retryAfter).getTime() - Date.now();
      }

      return parseInt(retryAfter) * 1000;
    }

    // Exponential backoff with jitter
    const exponentialDelay = Math.min(
      this.baseDelay * Math.pow(2, attempt),
      this.maxDelay
    );

    const jitter = Math.random() * this.jitterRange;

    return exponentialDelay + jitter;
  }

  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Request queue with rate limiting
class RequestQueue {
  constructor(rateLimit = 10, interval = 1000) {
    this.rateLimit = rateLimit;
    this.interval = interval;
    this.queue = [];
    this.processing = false;
    this.requestCount = 0;
    this.windowStart = Date.now();
  }

  async add(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ fn, resolve, reject });
      this.process();
    });
  }

  async process() {
    if (this.processing) return;
    this.processing = true;

    while (this.queue.length > 0) {
      // Check rate limit
      const now = Date.now();
      const windowElapsed = now - this.windowStart;

      if (windowElapsed >= this.interval) {
        // Reset window
        this.requestCount = 0;
        this.windowStart = now;
      }

      if (this.requestCount >= this.rateLimit) {
        // Wait for next window
        const waitTime = this.interval - windowElapsed;
        await new Promise(resolve => setTimeout(resolve, waitTime));
        continue;
      }

      // Process request
      const { fn, resolve, reject } = this.queue.shift();
      this.requestCount++;

      try {
        const result = await fn();
        resolve(result);
      } catch (error) {
        reject(error);
      }
    }

    this.processing = false;
  }
}

// Usage
const rateLimiter = new RateLimitHandler();
const queue = new RequestQueue(10, 1000);  // 10 requests per second

// With retry handler
const result = await rateLimiter.execute(() =>
  client.query({ query: GET_PRODUCTS })
);

// With queue
const results = await Promise.all(
  productIds.map(id =>
    queue.add(() => fetchProduct(id))
  )
);

GraphQL Specific Errors

UC_GQL_001: Query Depth Exceeded

Error Message: "Query depth of [depth] exceeds maximum of [max]"

Status Code: 400

Common Causes:

  • Too many nested fields
  • Recursive queries
  • Missing pagination

Solution:

// Bad - Too deep
const DEEP_QUERY = gql`
  query DeepQuery {
    users {
      orders {
        items {
          product {
            category {
              parent {
                parent {
                  parent {  # Too deep!
                    name
                  }
                }
              }
            }
          }
        }
      }
    }
  }
`;

// Good - Flatten with separate queries
const GET_USER_ORDERS = gql`
  query GetUserOrders($userId: ID!) {
    user(id: $userId) {
      id
      orders {
        id
        items {
          productId
          quantity
        }
      }
    }
  }
`;

const GET_PRODUCT_DETAILS = gql`
  query GetProductDetails($productIds: [ID!]!) {
    products(ids: $productIds) {
      id
      name
      category {
        id
        name
        parentId
      }
    }
  }
`;

// Fetch in stages
async function getUserOrderDetails(userId) {
  // First get orders
  const { data: userData } = await client.query({
    query: GET_USER_ORDERS,
    variables: { userId }
  });

  // Extract product IDs
  const productIds = userData.user.orders
    .flatMap(order => order.items)
    .map(item => item.productId);

  // Fetch product details
  const { data: productData } = await client.query({
    query: GET_PRODUCT_DETAILS,
    variables: { productIds: [...new Set(productIds)] }
  });

  // Combine results
  return {
    user: userData.user,
    products: productData.products
  };
}

UC_GQL_002: Query Complexity Exceeded

Error Message: "Query complexity of [complexity] exceeds maximum of [max]"

Status Code: 400

Common Causes:

  • Fetching too many fields
  • Large result sets without pagination
  • Complex calculations in query

Solution:

// Calculate query complexity before sending
function calculateComplexity(query, variables = {}) {
  let complexity = 0;

  // Base complexity per field
  const fieldComplexity = {
    'products': 10 * (variables.limit || 20),
    'orders': 15 * (variables.limit || 20),
    'customers': 5 * (variables.limit || 20),
    'analytics': 50,  // Heavy calculation
  };

  // Parse query and calculate
  // ... implementation ...

  return complexity;
}

// Split complex queries
async function fetchDashboardData() {
  // Instead of one complex query, use multiple simpler ones
  const queries = [
    { query: GET_REVENUE_METRICS, weight: 10 },
    { query: GET_TOP_PRODUCTS, weight: 5 },
    { query: GET_RECENT_ORDERS, weight: 8 },
    { query: GET_CUSTOMER_STATS, weight: 6 }
  ];

  // Execute in batches if needed
  const batchSize = 2;
  const results = [];

  for (let i = 0; i < queries.length; i += batchSize) {
    const batch = queries.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(({ query }) =>
        client.query({ query })
      )
    );
    results.push(...batchResults);
  }

  return results;
}

// Use field selection to reduce complexity
const OPTIMIZED_QUERY = gql`
  query GetProducts($fields: [ProductField!]) {
    products(limit: 20) {
      id
      name
      price

      # Only include heavy fields if requested
      reviews @include(if: $includeReviews) {
        rating
        count
      }

      inventory @include(if: $includeInventory) {
        available
        reserved
      }
    }
  }
`;

UC_GQL_003: Invalid Query Syntax

Error Message: "Syntax Error: [details]"

Status Code: 400

Common Causes:

  • Malformed GraphQL syntax
  • Missing closing brackets
  • Invalid field names
  • Wrong argument types

Solution:

// Validate queries at build time
import { parse, validate, buildSchema } from 'graphql';
import schemaString from './schema.graphql';

const schema = buildSchema(schemaString);

function validateQuery(queryString) {
  try {
    const documentAST = parse(queryString);
    const errors = validate(schema, documentAST);

    if (errors.length > 0) {
      console.error('Query validation errors:', errors);
      return false;
    }

    return true;
  } catch (error) {
    console.error('Query parsing error:', error);
    return false;
  }
}

// Use GraphQL Code Generator for type safety
// graphql-codegen.yml
const config = {
  schema: 'https://gateway.unifiedcommerce.app/graphql',
  documents: 'src/**/*.graphql',
  generates: {
    'src/generated/graphql.ts': {
      plugins: [
        'typescript',
        'typescript-operations',
        'typescript-apollo-client-helpers'
      ]
    }
  }
};

// Auto-generated typed queries
import { GetProductsDocument, GetProductsQuery } from './generated/graphql';

const { data } = await client.query<GetProductsQuery>({
  query: GetProductsDocument,
  variables: { limit: 20 }  // Type-checked!
});

// Common syntax fixes
const fixes = {
  // Missing fragment spread
  bad: `query { product { ...productFields } }`,
  good: `
    fragment productFields on Product {
      id name price
    }
    query { product { ...productFields } }
  `,

  // Wrong variable syntax
  bad2: `query GetProduct { product(id: $id) { name } }`,
  good2: `query GetProduct($id: ID!) { product(id: $id) { name } }`,

  // Invalid directive usage
  bad3: `query { products @include { name } }`,
  good3: `query($include: Boolean!) {
    products @include(if: $include) { name }
  }`,
};

Business Logic Errors

UC_BIZ_001: Insufficient Inventory

Error Message: "Insufficient inventory. Available: [available], Requested: [requested]"

Status Code: 400

Common Causes:

  • Product out of stock
  • Quantity exceeds available inventory
  • Reserved inventory not released
  • Concurrent purchases

Solution:

// Check inventory before purchase
async function checkAndReserveInventory(items) {
  const CHECK_INVENTORY = gql`
    query CheckInventory($skus: [String!]!) {
      inventory(skus: $skus) {
        sku
        available
        reserved
        incoming {
          quantity
          expectedDate
        }
      }
    }
  `;

  // Check availability
  const { data } = await client.query({
    query: CHECK_INVENTORY,
    variables: {
      skus: items.map(item => item.sku)
    },
    fetchPolicy: 'network-only'  // Always get fresh data
  });

  // Validate each item
  const unavailable = [];
  const warnings = [];

  items.forEach(item => {
    const inventory = data.inventory.find(inv => inv.sku === item.sku);

    if (!inventory || inventory.available < item.quantity) {
      unavailable.push({
        sku: item.sku,
        requested: item.quantity,
        available: inventory?.available || 0,
        nextRestock: inventory?.incoming?.[0]
      });
    } else if (inventory.available < item.quantity * 2) {
      warnings.push({
        sku: item.sku,
        message: 'Low stock warning'
      });
    }
  });

  if (unavailable.length > 0) {
    // Handle out of stock items
    return {
      success: false,
      unavailable,
      suggestions: await getSimilarProducts(unavailable)
    };
  }

  // Reserve inventory
  const RESERVE_INVENTORY = gql`
    mutation ReserveInventory($items: [ReserveItemInput!]!) {
      reserveInventory(items: $items) {
        reservationId
        expiresAt
        items {
          sku
          quantity
          reserved
        }
      }
    }
  `;

  const reservation = await client.mutate({
    mutation: RESERVE_INVENTORY,
    variables: { items }
  });

  return {
    success: true,
    reservationId: reservation.data.reserveInventory.reservationId,
    warnings
  };
}

// Implement back-in-stock notifications
async function notifyWhenAvailable(sku, email) {
  const SUBSCRIBE_STOCK_ALERT = gql`
    mutation SubscribeStockAlert($sku: String!, $email: String!) {
      subscribeStockAlert(sku: $sku, email: $email) {
        id
        status
        estimatedDate
      }
    }
  `;

  return await client.mutate({
    mutation: SUBSCRIBE_STOCK_ALERT,
    variables: { sku, email }
  });
}

UC_BIZ_002: Payment Processing Failed

Error Message: "Payment processing failed: [reason]"

Status Code: 402

Common Causes:

  • Card declined
  • Insufficient funds
  • Invalid card details
  • 3D Secure authentication failed
  • Payment method not supported

Solution:

// Robust payment handling
async function processPayment(paymentDetails) {
  const PROCESS_PAYMENT = gql`
    mutation ProcessPayment($input: PaymentInput!) {
      processPayment(input: $input) {
        id
        status
        requiresAction {
          type
          clientSecret
          redirectUrl
        }
        error {
          code
          message
          declineCode
        }
      }
    }
  `;

  try {
    const { data } = await client.mutate({
      mutation: PROCESS_PAYMENT,
      variables: { input: paymentDetails }
    });

    const payment = data.processPayment;

    // Handle different payment states
    switch (payment.status) {
      case 'succeeded':
        return { success: true, paymentId: payment.id };

      case 'requires_action':
        // Handle 3D Secure or additional authentication
        return await handle3DSecure(payment.requiresAction);

      case 'processing':
        // Payment is async (bank transfer, etc)
        return await pollPaymentStatus(payment.id);

      case 'failed':
        // Handle failure with specific recovery
        return await handlePaymentFailure(payment.error);

      default:
        throw new Error(`Unknown payment status: ${payment.status}`);
    }
  } catch (error) {
    // Network or system error
    return {
      success: false,
      error: 'Payment system unavailable. Please try again.',
      shouldRetry: true
    };
  }
}

async function handlePaymentFailure(error) {
  const recoveryStrategies = {
    'card_declined': async () => {
      // Suggest alternative payment method
      return {
        success: false,
        message: 'Card declined. Please try another payment method.',
        alternativeMethods: await getAlternativePaymentMethods()
      };
    },

    'insufficient_funds': async () => {
      // Offer payment plan
      return {
        success: false,
        message: 'Payment failed. Would you like to use our payment plan?',
        offerPaymentPlan: true
      };
    },

    'expired_card': async () => {
      // Request card update
      return {
        success: false,
        message: 'Your card has expired. Please update your payment information.',
        requiresUpdate: true
      };
    },

    'fraudulent': async () => {
      // Additional verification
      return {
        success: false,
        message: 'Payment requires additional verification.',
        requiresVerification: true
      };
    }
  };

  const strategy = recoveryStrategies[error.code] ||
    recoveryStrategies['default'];

  return await strategy();
}

// Implement retry with different payment method
async function retryWithAlternativePayment(orderId) {
  const methods = ['card', 'paypal', 'apple_pay', 'google_pay'];

  for (const method of methods) {
    try {
      const result = await processPayment({
        orderId,
        method,
        // ... method-specific details
      });

      if (result.success) {
        return result;
      }
    } catch (error) {
      continue;  // Try next method
    }
  }

  throw new Error('All payment methods failed');
}

UC_BIZ_003: Order Cannot Be Modified

Error Message: "Order [orderId] cannot be modified in status [status]"

Status Code: 409

Common Causes:

  • Order already shipped
  • Order cancelled
  • Payment completed
  • Past modification window

Solution:

// Check order status before modification
async function modifyOrder(orderId, modifications) {
  const GET_ORDER_STATUS = gql`
    query GetOrderStatus($id: ID!) {
      order(id: $id) {
        id
        status
        createdAt
        shippedAt
        canModify
        modificationDeadline
        items {
          id
          status
          canModify
        }
      }
    }
  `;

  // Check current status
  const { data } = await client.query({
    query: GET_ORDER_STATUS,
    variables: { id: orderId }
  });

  const order = data.order;

  // Validate modification eligibility
  if (!order.canModify) {
    // Provide alternatives based on status
    switch (order.status) {
      case 'shipped':
        return {
          success: false,
          message: 'Order already shipped. You can initiate a return instead.',
          action: 'INITIATE_RETURN'
        };

      case 'delivered':
        return {
          success: false,
          message: 'Order delivered. You can request an exchange.',
          action: 'REQUEST_EXCHANGE'
        };

      case 'cancelled':
        return {
          success: false,
          message: 'Order is cancelled. Please place a new order.',
          action: 'CREATE_NEW_ORDER'
        };

      default:
        const deadline = new Date(order.modificationDeadline);
        if (deadline < new Date()) {
          return {
            success: false,
            message: `Modification deadline passed (${deadline.toLocaleString()})`,
            action: 'CONTACT_SUPPORT'
          };
        }
    }
  }

  // Proceed with modification
  const MODIFY_ORDER = gql`
    mutation ModifyOrder($id: ID!, $modifications: OrderModifications!) {
      modifyOrder(id: $id, modifications: $modifications) {
        id
        status
        items {
          id
          quantity
          price
        }
        totalAmount
      }
    }
  `;

  try {
    const result = await client.mutate({
      mutation: MODIFY_ORDER,
      variables: { id: orderId, modifications }
    });

    return {
      success: true,
      order: result.data.modifyOrder
    };
  } catch (error) {
    // Handle specific modification errors
    if (error.extensions?.code === 'ITEM_SHIPPED') {
      return {
        success: false,
        message: 'Some items already shipped. Modifying remaining items only.',
        partialModification: true,
        modifiableItems: order.items.filter(item => item.canModify)
      };
    }
    throw error;
  }
}

// Implement order cancellation with refund
async function cancelOrder(orderId, reason) {
  const CANCEL_ORDER = gql`
    mutation CancelOrder($id: ID!, $reason: String!) {
      cancelOrder(id: $id, reason: $reason) {
        id
        status
        refund {
          amount
          status
          estimatedDate
        }
      }
    }
  `;

  const result = await client.mutate({
    mutation: CANCEL_ORDER,
    variables: { id: orderId, reason }
  });

  // Handle refund status
  const refund = result.data.cancelOrder.refund;
  if (refund) {
    showNotification(`Refund of ${refund.amount} will be processed by ${refund.estimatedDate}`);
  }

  return result.data.cancelOrder;
}

Server Errors (5xx)

UC_SRV_001: Internal Server Error

Error Message: "An unexpected error occurred. Please try again."

Status Code: 500

Common Causes:

  • Server-side bug
  • Database connection issue
  • Service unavailable
  • Timeout

Solution:

// Implement circuit breaker pattern
class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000;
    this.state = 'CLOSED';
    this.failureCount = 0;
    this.nextAttempt = Date.now();
  }

  async execute(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    if (this.state === 'HALF_OPEN') {
      this.state = 'CLOSED';
    }
  }

  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
    }
  }
}

// Use with API calls
const breaker = new CircuitBreaker();

async function fetchWithCircuitBreaker(query) {
  try {
    return await breaker.execute(() =>
      client.query({ query })
    );
  } catch (error) {
    if (error.message === 'Circuit breaker is OPEN') {
      // Use cached data or show maintenance message
      return getCachedData(query) || {
        error: 'Service temporarily unavailable'
      };
    }
    throw error;
  }
}

// Implement fallback mechanisms
async function fetchWithFallback(query, variables) {
  const strategies = [
    // Primary - Direct API
    () => client.query({ query, variables }),

    // Fallback 1 - Secondary region
    () => secondaryClient.query({ query, variables }),

    // Fallback 2 - Cache
    () => cache.get(getCacheKey(query, variables)),

    // Fallback 3 - Default data
    () => getDefaultData(query)
  ];

  for (const strategy of strategies) {
    try {
      const result = await strategy();
      if (result) return result;
    } catch (error) {
      console.warn('Strategy failed:', error);
      continue;
    }
  }

  throw new Error('All strategies failed');
}

Best Practices for Error Handling

Global Error Handler

// Centralized error handling
class ErrorHandler {
  constructor() {
    this.handlers = new Map();
    this.defaultHandler = this.handleUnknownError;
    this.setupHandlers();
  }

  setupHandlers() {
    // Authentication errors
    this.handlers.set('UNAUTHENTICATED', this.handleAuthError);
    this.handlers.set('FORBIDDEN', this.handleForbiddenError);

    // Validation errors
    this.handlers.set('BAD_USER_INPUT', this.handleValidationError);
    this.handlers.set('DUPLICATE_ENTRY', this.handleDuplicateError);

    // Rate limiting
    this.handlers.set('RATE_LIMITED', this.handleRateLimitError);

    // Business logic
    this.handlers.set('INSUFFICIENT_INVENTORY', this.handleInventoryError);
    this.handlers.set('PAYMENT_FAILED', this.handlePaymentError);

    // Server errors
    this.handlers.set('INTERNAL_SERVER_ERROR', this.handleServerError);
  }

  async handle(error) {
    const code = error.extensions?.code || error.code;
    const handler = this.handlers.get(code) || this.defaultHandler;

    // Log error for monitoring
    this.logError(error);

    // Execute handler
    return await handler.call(this, error);
  }

  handleAuthError(error) {
    // Clear auth and redirect to login
    localStorage.removeItem('token');
    window.location.href = '/login';

    return {
      userMessage: 'Please log in to continue',
      action: 'LOGIN_REQUIRED'
    };
  }

  handleValidationError(error) {
    // Extract field-specific errors
    const fieldErrors = error.extensions?.details || {};

    return {
      userMessage: 'Please correct the highlighted fields',
      fieldErrors,
      action: 'FIX_VALIDATION'
    };
  }

  handleRateLimitError(error) {
    const retryAfter = error.extensions?.retryAfter || 60;

    return {
      userMessage: `Too many requests. Please wait ${retryAfter} seconds.`,
      retryAfter,
      action: 'WAIT_AND_RETRY'
    };
  }

  handleServerError(error) {
    return {
      userMessage: 'Something went wrong. Our team has been notified.',
      action: 'CONTACT_SUPPORT',
      supportInfo: {
        requestId: error.extensions?.requestId,
        timestamp: new Date().toISOString()
      }
    };
  }

  handleUnknownError(error) {
    return {
      userMessage: 'An unexpected error occurred',
      action: 'RETRY',
      technical: error.message
    };
  }

  logError(error) {
    // Send to monitoring service
    if (window.Sentry) {
      window.Sentry.captureException(error, {
        tags: {
          code: error.extensions?.code,
          requestId: error.extensions?.requestId
        }
      });
    }

    // Console log in development
    if (process.env.NODE_ENV === 'development') {
      console.error('API Error:', error);
    }
  }
}

// Use globally
const errorHandler = new ErrorHandler();

// With Apollo Client
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(error => {
      errorHandler.handle(error);
    });
  }

  if (networkError) {
    errorHandler.handle({
      code: 'NETWORK_ERROR',
      message: networkError.message
    });
  }
});

User-Friendly Error Messages

// Transform technical errors to user-friendly messages
const errorMessages = {
  // Network errors
  'Failed to fetch': 'Unable to connect. Please check your internet connection.',
  'Network request failed': 'Connection lost. Please try again.',
  'timeout': 'Request took too long. Please try again.',

  // Auth errors
  'Invalid token': 'Your session has expired. Please log in again.',
  'Unauthorized': 'You don\'t have permission to do this.',

  // Validation
  'Invalid email': 'Please enter a valid email address.',
  'Required field': 'This field is required.',

  // Business logic
  'Insufficient funds': 'Payment could not be processed. Please try another payment method.',
  'Out of stock': 'This item is no longer available.',

  // Generic
  'Internal server error': 'Something went wrong on our end. Please try again later.'
};

function getUserMessage(error) {
  // Check for custom message
  if (error.extensions?.userMessage) {
    return error.extensions.userMessage;
  }

  // Look up predefined message
  for (const [key, message] of Object.entries(errorMessages)) {
    if (error.message.includes(key)) {
      return message;
    }
  }

  // Default message
  return 'An error occurred. Please try again.';
}

Error Recovery Strategies

Automatic Retry with Backoff

async function retryableOperation(operation, options = {}) {
  const maxRetries = options.maxRetries || 3;
  const shouldRetry = options.shouldRetry || defaultShouldRetry;

  let lastError;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;

      if (!shouldRetry(error, attempt)) {
        throw error;
      }

      const delay = calculateDelay(attempt);
      await sleep(delay);
    }
  }

  throw lastError;
}

function defaultShouldRetry(error, attempt) {
  // Retry on network errors and 5xx status codes
  const retryableCodes = [500, 502, 503, 504, 429];
  const statusCode = error.statusCode || error.extensions?.statusCode;

  return retryableCodes.includes(statusCode) && attempt < 3;
}

function calculateDelay(attempt) {
  // Exponential backoff with jitter
  const baseDelay = 1000;
  const maxDelay = 30000;
  const exponentialDelay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
  const jitter = Math.random() * 1000;

  return exponentialDelay + jitter;
}

Need More Help?

If you continue to experience errors:

  1. Check our status page: status.unifiedcommerce.app
  2. Search our community forum: community.unifiedcommerce.app
  3. Contact support with:
    • Request ID (from error response)
    • Full error message
    • Code snippet reproducing the issue
    • Timestamp of occurrence

Remember to never share your full API keys in support requests or public forums.

Was this page helpful?