Error Handling Best Practices

Robust error handling ensures your application remains stable and provides good user experience even when things go wrong. This guide covers patterns for handling errors during initialization, data operations, and queries.

Error Handling Principles

Effective error handling follows these principles:

  • Anticipate failures - Assume network requests and data operations can fail
  • Fail gracefully - Provide fallbacks and meaningful error messages
  • Log for debugging - Capture enough context to diagnose issues
  • Recover when possible - Retry transient failures automatically

Initialization Errors

Wrap Init in Try-Catch

Always wrap initialization in error handling:

init-error-handling.ts
async function initializeDcupl(): Promise<Dcupl | null> {
  const dcupl = new Dcupl();

  try {
    dcupl.models.set(productModel);
    dcupl.models.set(categoryModel);

    const products = await fetchProducts();
    dcupl.data.set(products, { model: 'Product' });

    await dcupl.init();
    return dcupl;
  } catch (error) {
    console.error('Failed to initialize dcupl:', error);
    return null;
  }
}

// Usage
const dcupl = await initializeDcupl();
if (!dcupl) {
  showErrorMessage('Failed to load data. Please refresh the page.');
}

Handle Model Definition Errors

Invalid model definitions throw errors. Validate during development:

model-validation.ts
function setModelSafely(dcupl: Dcupl, model: ModelDefinition): boolean {
  try {
    dcupl.models.set(model);
    return true;
  } catch (error) {
    console.error(`Invalid model definition for ${model.key}:`, error);
    return false;
  }
}

// Usage
const isValid = setModelSafely(dcupl, productModel);
if (!isValid) {
  // Handle invalid model - likely a development error
  throw new Error('Model validation failed. Check model definitions.');
}

Retry Failed Data Loading

Network requests can fail. Implement retry logic:

retry-loading.ts
async function loadDataWithRetry(
  dcupl: Dcupl,
  fetchFn: () => Promise<any[]>,
  model: string,
  maxRetries = 3
): Promise<boolean> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const data = await fetchFn();
      dcupl.data.set(data, { model });
      return true;
    } catch (error) {
      console.error(`Load attempt ${attempt} failed for ${model}:`, error);

      if (attempt === maxRetries) {
        return false;
      }

      // Exponential backoff: 1s, 2s, 4s
      const delay = 1000 * Math.pow(2, attempt - 1);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
  return false;
}

// Usage
const loaded = await loadDataWithRetry(
  dcupl,
  () => fetch('/api/products').then((r) => r.json()),
  'Product'
);

if (!loaded) {
  showErrorMessage('Unable to load products. Please check your connection.');
}

Data Operation Errors

Handle Update Failures

Updates can fail if data is invalid or keys do not exist:

update-error.ts
function updateProductPrice(
  dcupl: Dcupl,
  productKey: string,
  newPrice: number
): { success: boolean; error?: string } {
  try {
    // Validate input
    if (newPrice < 0) {
      return { success: false, error: 'Price cannot be negative' };
    }

    // Check if product exists
    const list = dcupl.lists.create({ modelKey: 'Product' });
    const product = list.catalog.getItemByKey(productKey);
    list.destroy();

    if (!product) {
      return { success: false, error: `Product ${productKey} not found` };
    }

    // Perform update
    dcupl.data.update([
      { key: productKey, price: newPrice },
    ], { model: 'Product' });

    return { success: true };
  } catch (error) {
    console.error('Update failed:', error);
    return { success: false, error: 'Failed to update product' };
  }
}

// Usage
const result = updateProductPrice(dcupl, 'p1', 99.99);
if (!result.success) {
  showErrorMessage(result.error!);
}

Batch Update Error Handling

When updating multiple items, track which succeeded and which failed:

batch-update.ts
interface BatchResult {
  succeeded: string[];
  failed: Array<{ key: string; error: string }>;
}

function batchUpdatePrices(
  dcupl: Dcupl,
  updates: Array<{ key: string; price: number }>
): BatchResult {
  const result: BatchResult = { succeeded: [], failed: [] };

  // Validate all updates first
  const validUpdates: Array<{ key: string; price: number }> = [];

  for (const update of updates) {
    if (update.price < 0) {
      result.failed.push({
        key: update.key,
        error: 'Price cannot be negative',
      });
      continue;
    }
    validUpdates.push(update);
  }

  // Perform valid updates
  if (validUpdates.length > 0) {
    try {
      dcupl.data.update(validUpdates, { model: 'Product' });
      result.succeeded = validUpdates.map((u) => u.key);
    } catch (error) {
      // All updates failed
      for (const update of validUpdates) {
        result.failed.push({
          key: update.key,
          error: 'Update operation failed',
        });
      }
    }
  }

  return result;
}

// Usage
const result = batchUpdatePrices(dcupl, [
  { key: 'p1', price: 99.99 },
  { key: 'p2', price: -10 }, // Invalid
  { key: 'p3', price: 149.99 },
]);

console.log(`Updated: ${result.succeeded.length}, Failed: ${result.failed.length}`);
for (const failure of result.failed) {
  console.warn(`Failed to update ${failure.key}: ${failure.error}`);
}

Handle Delete Errors

Deleting non-existent keys is usually safe, but wrap for consistency:

delete-error.ts
function deleteProducts(dcupl: Dcupl, keys: string[]): { success: boolean; error?: string } {
  try {
    if (keys.length === 0) {
      return { success: true };
    }

    dcupl.data.remove(
      keys.map((key) => ({ key })),
      { model: 'Product' }
    );

    return { success: true };
  } catch (error) {
    console.error('Delete failed:', error);
    return { success: false, error: 'Failed to delete products' };
  }
}

Query Error Handling

Handle Missing Data Gracefully

Queries for non-existent items should not crash your app:

query-fallback.ts
function getProductByKey(dcupl: Dcupl, productKey: string): Product | null {
  try {
    const list = dcupl.lists.create({ modelKey: 'Product' });
    const product = list.catalog.getItemByKey(productKey);
    list.destroy();
    return product || null;
  } catch (error) {
    console.error('Failed to get product:', error);
    return null;
  }
}

// Usage in UI
const product = getProductByKey(dcupl, productId);
if (product) {
  renderProductDetails(product);
} else {
  renderProductNotFound();
}

Validate Query Parameters

Validate user-provided query parameters before executing:

query-validation.ts
interface ProductFilters {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
}

function validateFilters(filters: ProductFilters): string | null {
  if (filters.minPrice !== undefined && filters.minPrice < 0) {
    return 'Minimum price cannot be negative';
  }

  if (filters.maxPrice !== undefined && filters.maxPrice < 0) {
    return 'Maximum price cannot be negative';
  }

  if (
    filters.minPrice !== undefined &&
    filters.maxPrice !== undefined &&
    filters.minPrice > filters.maxPrice
  ) {
    return 'Minimum price cannot be greater than maximum price';
  }

  return null;
}

function queryProducts(
  list: DcuplList,
  filters: ProductFilters
): { items: Product[]; error?: string } {
  // Validate first
  const validationError = validateFilters(filters);
  if (validationError) {
    return { items: [], error: validationError };
  }

  try {
    list.catalog.query.clear();

    if (filters.category) {
      list.catalog.query.addCondition({
        attribute: 'category',
        operator: 'eq',
        value: filters.category,
      });
    }

    if (filters.minPrice !== undefined) {
      list.catalog.query.addCondition({
        attribute: 'price',
        operator: 'gte',
        value: filters.minPrice,
      });
    }

    if (filters.maxPrice !== undefined) {
      list.catalog.query.addCondition({
        attribute: 'price',
        operator: 'lte',
        value: filters.maxPrice,
      });
    }

    const items = list.catalog.query.items();
    return { items };
  } catch (error) {
    console.error('Query failed:', error);
    return { items: [], error: 'Search failed. Please try again.' };
  }
}

Handle Empty Results

Empty results are not errors, but handle them in your UI:

empty-results.ts
function searchProducts(list: DcuplList, searchTerm: string) {
  list.catalog.query.clear();

  if (searchTerm.trim()) {
    list.catalog.query.addCondition({
      attribute: 'name',
      operator: 'find',
      value: searchTerm,
    });
  }

  const results = list.catalog.query.items();
  const count = list.catalog.query.count();

  return {
    items: results,
    count,
    isEmpty: count === 0,
    hasSearchTerm: searchTerm.trim().length > 0,
  };
}

// Usage in UI
const search = searchProducts(productList, userInput);

if (search.isEmpty && search.hasSearchTerm) {
  showMessage(`No products found matching "${userInput}"`);
} else if (search.isEmpty) {
  showMessage('No products available');
} else {
  renderProducts(search.items);
}

Quality Validation Errors

Enable Quality Checks in Development

Use quality validation to catch data issues early:

quality-checks.ts
const dcupl = new Dcupl();

// Enable quality validation
dcupl.models.set({
  key: 'Product',
  properties: [
    {
      key: 'name',
      type: 'string',
      quality: {
        required: true,
        nullable: false,
      },
    },
    {
      key: 'price',
      type: 'float',
      quality: {
        required: true,
        validators: {
          min: { value: 0 },
        },
      },
    },
    {
      key: 'email',
      type: 'string',
      quality: {
        validators: {
          email: {},
        },
      },
    },
  ],
});

Check for Quality Errors

After loading data, check for validation errors:

check-quality.ts
await dcupl.init();

const qualityErrors = dcupl.quality.values;

if (qualityErrors && Object.keys(qualityErrors).length > 0) {
  console.warn('Data quality issues detected:', qualityErrors);

  // In development: Surface errors prominently
  if (process.env.NODE_ENV === 'development') {
    console.table(qualityErrors);
  }

  // In production: Log to monitoring service
  if (process.env.NODE_ENV === 'production') {
    logToMonitoring('data_quality_issues', { errors: qualityErrors });
  }
}

Reference Integrity Errors

Handle Missing References

When referenced items do not exist, queries may return incomplete data:

reference-check.ts
function getOrderWithCustomer(
  dcupl: Dcupl,
  orderKey: string
): { order: Order | null; customerMissing: boolean } {
  const orderList = dcupl.lists.create({ modelKey: 'Order' });
  const order = orderList.catalog.getItemByKey(orderKey);
  orderList.destroy();

  if (!order) {
    return { order: null, customerMissing: false };
  }

  // Check if referenced customer exists
  if (order.customer) {
    const customerList = dcupl.lists.create({ modelKey: 'Customer' });
    const customer = customerList.catalog.getItemByKey(order.customer);
    customerList.destroy();

    if (!customer) {
      console.warn(`Order ${orderKey} references missing customer: ${order.customer}`);
      return { order, customerMissing: true };
    }
  }

  return { order, customerMissing: false };
}

Validate Data Before Loading

Check referential integrity before loading data:

referential-integrity.ts
interface ValidationResult {
  isValid: boolean;
  errors: string[];
}

function validateOrderData(orders: any[], customerKeys: Set<string>): ValidationResult {
  const errors: string[] = [];

  for (const order of orders) {
    if (order.customer && !customerKeys.has(order.customer)) {
      errors.push(`Order ${order.key} references unknown customer: ${order.customer}`);
    }
  }

  return {
    isValid: errors.length === 0,
    errors,
  };
}

// Usage
const customers = await fetchCustomers();
const customerKeys = new Set(customers.map((c) => c.key));

const orders = await fetchOrders();
const validation = validateOrderData(orders, customerKeys);

if (!validation.isValid) {
  console.warn('Referential integrity issues:', validation.errors);
  // Decide: Load anyway, skip invalid, or fail
}

dcupl.data.set(customers, { model: 'Customer' });
dcupl.data.set(orders, { model: 'Order' });

Error Recovery Patterns

Fallback to Cached Data

If fresh data fails to load, use cached data:

cache-fallback.ts
async function loadProductsWithFallback(dcupl: Dcupl): Promise<boolean> {
  try {
    // Try to load fresh data
    const products = await fetch('/api/products').then((r) => r.json());
    dcupl.data.set(products, { model: 'Product' });

    // Cache for future fallback
    localStorage.setItem('products_cache', JSON.stringify(products));
    localStorage.setItem('products_cache_time', Date.now().toString());

    return true;
  } catch (error) {
    console.error('Failed to load fresh data:', error);

    // Try cached data
    const cached = localStorage.getItem('products_cache');
    if (cached) {
      const cacheTime = localStorage.getItem('products_cache_time');
      const cacheAge = Date.now() - Number(cacheTime);
      const maxAge = 24 * 60 * 60 * 1000; // 24 hours

      if (cacheAge < maxAge) {
        console.log('Using cached data');
        dcupl.data.set(JSON.parse(cached), { model: 'Product' });
        return true;
      } else {
        console.warn('Cache too old, not using');
      }
    }

    return false;
  }
}

Partial Data Loading

If some data fails, load what you can:

partial-loading.ts
interface LoadResult {
  loaded: string[];
  failed: string[];
}

async function loadAllModels(dcupl: Dcupl): Promise<LoadResult> {
  const models = [
    { key: 'Product', fetch: fetchProducts },
    { key: 'Category', fetch: fetchCategories },
    { key: 'Brand', fetch: fetchBrands },
  ];

  const result: LoadResult = { loaded: [], failed: [] };

  for (const model of models) {
    try {
      const data = await model.fetch();
      dcupl.data.set(data, { model: model.key });
      result.loaded.push(model.key);
    } catch (error) {
      console.error(`Failed to load ${model.key}:`, error);
      result.failed.push(model.key);
    }
  }

  // Initialize with whatever loaded
  if (result.loaded.length > 0) {
    await dcupl.init();
  }

  return result;
}

// Usage
const loadResult = await loadAllModels(dcupl);

if (loadResult.failed.length > 0) {
  showWarning(`Some data could not be loaded: ${loadResult.failed.join(', ')}`);
}

if (loadResult.loaded.includes('Product')) {
  renderProductList();
} else {
  showError('Products unavailable');
}

Graceful Degradation

Provide reduced functionality when errors occur:

graceful-degradation.ts
class ProductService {
  private dcupl: Dcupl;
  private isInitialized = false;
  private initError: Error | null = null;

  async initialize() {
    try {
      this.dcupl = new Dcupl();
      // ... setup models and data
      await this.dcupl.init();
      this.isInitialized = true;
    } catch (error) {
      this.initError = error as Error;
      console.error('ProductService initialization failed:', error);
    }
  }

  getProducts(filters: ProductFilters): Product[] {
    if (!this.isInitialized) {
      console.warn('ProductService not initialized, returning empty');
      return [];
    }

    try {
      // ... perform query
      return results;
    } catch (error) {
      console.error('Query failed:', error);
      return [];
    }
  }

  isAvailable(): boolean {
    return this.isInitialized;
  }

  getInitError(): Error | null {
    return this.initError;
  }
}

// Usage in UI
const service = new ProductService();
await service.initialize();

if (!service.isAvailable()) {
  const error = service.getInitError();
  showError(`Product search unavailable: ${error?.message}`);
  showFallbackContent();
} else {
  const products = service.getProducts(filters);
  renderProducts(products);
}

Logging Best Practices

Structured Error Logging

Log errors with context for debugging:

structured-logging.ts
interface ErrorContext {
  operation: string;
  model?: string;
  key?: string;
  filters?: object;
  timestamp: string;
}

function logError(error: Error, context: Omit<ErrorContext, 'timestamp'>) {
  const entry = {
    ...context,
    timestamp: new Date().toISOString(),
    error: {
      message: error.message,
      stack: error.stack,
    },
  };

  // Development: Console
  console.error('dcupl error:', entry);

  // Production: Send to monitoring service
  if (process.env.NODE_ENV === 'production') {
    sendToMonitoring(entry);
  }
}

// Usage
try {
  dcupl.data.update(updates, { model: 'Product' });
} catch (error) {
  logError(error as Error, {
    operation: 'updateProducts',
    model: 'Product',
  });
}

Track Error Frequency

Monitor error patterns to identify systemic issues:

error-tracking.ts
class ErrorTracker {
  private errors: Map<string, number> = new Map();
  private threshold = 10;
  private alertSent = new Set<string>();

  track(errorType: string) {
    const count = (this.errors.get(errorType) || 0) + 1;
    this.errors.set(errorType, count);

    if (count >= this.threshold && !this.alertSent.has(errorType)) {
      this.alert(errorType, count);
      this.alertSent.add(errorType);
    }
  }

  private alert(errorType: string, count: number) {
    console.warn(`High error frequency: ${errorType} (${count} occurrences)`);
    // Send alert to monitoring
  }

  getStats() {
    return Object.fromEntries(this.errors);
  }

  reset() {
    this.errors.clear();
    this.alertSent.clear();
  }
}

const errorTracker = new ErrorTracker();

// Usage in error handlers
try {
  // ... operation
} catch (error) {
  errorTracker.track('query_failed');
}

Error Handling Checklist

Initialization

  • Wrap init() in try-catch
  • Validate model definitions
  • Implement retry logic for data loading
  • Provide fallback for initialization failures

Data Operations

  • Validate input before updates
  • Handle missing keys gracefully
  • Track batch operation results
  • Log failed operations with context

Queries

  • Validate user-provided parameters
  • Handle empty results in UI
  • Return null for missing items (not throw)
  • Provide meaningful error messages

Recovery

  • Cache data for offline fallback
  • Load partial data when possible
  • Degrade gracefully on errors
  • Surface errors appropriately by environment

What's Next?