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:
- Check our status page: status.unifiedcommerce.app
- Search our community forum: community.unifiedcommerce.app
- 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.