Error Handling

Learn how to handle errors gracefully in dcupl applications, from initialization failures to data quality issues.

Error Categories

dcupl has three main error categories:

Category Description When it occurs
Model Errors Data quality and validation issues During init() or data updates
Loader Errors Configuration and resource loading failures During loader.process() or init()
Response Errors Runtime API failures During queries or operations

Model Errors

Model errors occur when data doesn't match the expected schema or validation rules.

Error Groups

Group Description
ReferenceDataError Reference points to missing data
PropertyDataError Property value doesn't match type or validation
DataContainerError Issues with data container processing
ModelDefinitionError Invalid model configuration

Error Types

Type Description
UndefinedAttribute Property not defined in model
InvalidValidator Validation rule failed
UndefinedValue Required field is missing
NullValue Null value where not allowed
WrongDataType Type mismatch (with strict mode)
NonUniqueKey Duplicate key when unique required
MissingModel Referenced model doesn't exist
MissingProperty Property doesn't exist on model
MissingReference Referenced item doesn't exist
UnknownExpressionVariable Expression uses undefined variable
RemoteReferenceKeyNotFound Remote reference target not found
InvalidModelDefinition Model schema is invalid

Handling Model Errors

import { Dcupl } from '@dcupl/core';

const dcupl = new Dcupl({
  quality: { enabled: true },
});

// Setup models and data...
await dcupl.init();

// Check for errors after init
const errors = dcupl.quality.getErrors();

if (errors.length > 0) {
  // Group errors by type
  const errorsByType = errors.reduce(
    (acc, err) => {
      acc[err.errorType] = acc[err.errorType] || [];
      acc[err.errorType].push(err);
      return acc;
    },
    {} as Record<string, typeof errors>
  );

  console.error('Validation errors:', errorsByType);

  // Handle specific error types
  if (errorsByType.MissingReference) {
    console.warn('Some references are broken - data may be incomplete');
  }

  if (errorsByType.UndefinedValue) {
    console.error('Required fields missing - blocking error');
    throw new Error('Data validation failed');
  }
}

Error Structure

type ModelError = {
  type: 'model';
  errorGroup:
    | 'ReferenceDataError'
    | 'PropertyDataError'
    | 'DataContainerError'
    | 'ModelDefinitionError';
  errorType: string; // See error types table
  title: string; // Human-readable title
  description?: string; // Detailed description
  model?: string; // Affected model key
  itemKey?: string; // Affected item key
  attribute?: string; // Affected attribute
  meta?: Record<string, unknown>;
};

Loader Errors

Loader errors occur when fetching or processing configuration and data resources.

Error Groups

Group Description
LoaderConfigError Invalid configuration file or structure
ResourceError Failed to fetch or process a resource

Error Types

Type Description
InvalidConfig Configuration file is malformed
InvalidResource Resource could not be loaded or parsed

Handling Loader Errors

import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';

const dcupl = new Dcupl();
const loader = new DcuplAppLoader();

// Track loading progress and errors
loader.on((progress) => {
  if (progress.type === 'resource_error') {
    console.error(`Failed to load: ${progress.resourceKey}`, progress.error);
  }
});

try {
  await loader.config.fetch({
    projectId: 'my-project',
    apiKey: 'my-api-key',
  });
} catch (error) {
  console.error('Failed to fetch config:', error);
  // Fallback to local config or show error UI
  return;
}

try {
  await loader.process();
} catch (error) {
  console.error('Failed to process resources:', error);
  // Handle gracefully
}

dcupl.loaders.add(loader, 'main');

try {
  await dcupl.init();
} catch (error) {
  console.error('Initialization failed:', error);
}

Resource Retry Pattern

async function loadWithRetry(
  loader: DcuplAppLoader,
  options: { projectId: string; apiKey: string },
  maxRetries = 3
) {
  let lastError: Error | undefined;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      await loader.config.fetch(options);
      await loader.process();
      return; // Success
    } catch (error) {
      lastError = error as Error;
      console.warn(`Attempt ${attempt}/${maxRetries} failed:`, error);

      if (attempt < maxRetries) {
        // Exponential backoff
        await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
      }
    }
  }

  throw lastError;
}

Response Errors

Response errors occur during runtime operations like queries.

Error Structure

type DcuplErrorResponse = {
  name?: string;
  message?: string;
  type?: 'fatal' | 'minor';
};

Handling Response Errors

// dcupl methods return results directly, but you can wrap in try-catch
function safeQuery<T>(list: DcuplList, options: QueryOptions): T[] | null {
  try {
    return list.catalog.query.items(options);
  } catch (error) {
    console.error('Query failed:', error);
    return null;
  }
}

// Usage
const products = safeQuery(productList, { offset: 0, limit: 10 });
if (products === null) {
  // Show error state
}

Event-Based Error Handling

Subscribe to dcupl events for real-time error detection.

const dcupl = new Dcupl({
  quality: { enabled: true },
});

// Global error listener
dcupl.on((message) => {
  // Track slow operations
  if (message.duration && message.duration > 100) {
    console.warn(`Slow operation: ${message.type} took ${message.duration}ms`);
  }

  // Track data changes for debugging
  if (message.type === 'data_change') {
    console.log('Data changed:', message.modelKey, message.args);
  }
});

// List-specific error tracking
const list = dcupl.lists.create({ modelKey: 'Product' });

list.on((message) => {
  if (message.type === 'query_execute') {
    // Track query execution
    console.log(`Query executed in ${message.duration}ms`);
  }
});

Logging Configuration

Configure logging for better debugging.

import { Dcupl } from '@dcupl/core';

const dcupl = new Dcupl({
  logging: {
    // Log levels: 'debug' | 'info' | 'warn' | 'error'
    level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn',

    // Output format
    format: process.env.NODE_ENV === 'development' ? 'pretty' : 'json',

    // Custom log handler for external services
    onLog: (entry) => {
      if (entry.level === 'error') {
        // Send to error tracking service
        errorTracker.captureError(entry);
      }
    },

    // Module-specific levels
    modules: {
      ModelParser: 'debug',
      Catalog: 'warn',
    },
  },
});

Error Recovery Strategies

Graceful Degradation

async function initializeDcupl() {
  const dcupl = new Dcupl({ quality: { enabled: true } });
  const loader = new DcuplAppLoader();

  try {
    // Try to load from remote
    await loader.config.fetch({
      projectId: process.env.DCUPL_PROJECT_ID!,
      apiKey: process.env.DCUPL_API_KEY!,
    });
  } catch (error) {
    console.warn('Remote config failed, using local fallback');

    // Fallback to local configuration
    loader.config.set({
      resources: [{ key: 'products', type: 'data', url: '/data/products.json', model: 'Product' }],
      models: [{ key: 'Product', properties: [{ key: 'name', type: 'string' }] }],
    });
  }

  await loader.process();
  dcupl.loaders.add(loader, 'main');
  await dcupl.init();

  // Check quality even after successful init
  const errors = dcupl.quality.getErrors();
  if (errors.length > 0) {
    console.warn(`Initialized with ${errors.length} data quality issues`);
  }

  return dcupl;
}

Partial Data Loading

async function loadCriticalDataFirst(dcupl: Dcupl, loader: DcuplAppLoader) {
  // Load critical resources first
  const criticalResources = loader.resources.getWithTag('critical');
  const optionalResources = loader.resources.getWithTag('optional');

  // Process critical resources
  try {
    await loader.process({ resourceTags: ['critical'] });
    dcupl.loaders.add(loader, 'main');
    await dcupl.init();
  } catch (error) {
    console.error('Critical data failed to load:', error);
    throw error; // Can't continue without critical data
  }

  // Load optional resources in background
  for (const resource of optionalResources) {
    try {
      await loader.process({ resourceTags: [resource.tags?.[0] || ''] });
      await dcupl.update();
    } catch (error) {
      console.warn(`Optional resource ${resource.key} failed:`, error);
      // Continue without this resource
    }
  }
}

Validation Error Handling

function handleValidationErrors(dcupl: Dcupl, options: { strict: boolean }) {
  const errors = dcupl.quality.getErrors();

  // Categorize errors by severity
  const critical = errors.filter(
    (e) => e.errorType === 'MissingModel' || e.errorType === 'InvalidModelDefinition'
  );

  const warnings = errors.filter(
    (e) => e.errorType === 'MissingReference' || e.errorType === 'UndefinedValue'
  );

  const info = errors.filter(
    (e) => e.errorType === 'WrongDataType' || e.errorType === 'UndefinedAttribute'
  );

  // Report errors
  if (critical.length > 0) {
    console.error('Critical errors - cannot proceed:', critical);
    if (options.strict) {
      throw new Error('Critical validation errors');
    }
  }

  if (warnings.length > 0) {
    console.warn('Data warnings:', warnings);
  }

  if (info.length > 0 && process.env.NODE_ENV === 'development') {
    console.info('Data info:', info);
  }

  return { critical, warnings, info };
}

Debugging Tips

Enable Debug Logging

const dcupl = new Dcupl({
  logging: {
    level: 'debug',
    format: 'pretty',
  },
});

Inspect Quality Metrics

await dcupl.init();

// Get quality analytics
const analytics = dcupl.quality.values;

console.log('Performance:', {
  loading: `${analytics.loading}ms`,
  processing: `${analytics.processing}ms`,
  startup: `${analytics.startup}ms`,
});

console.log('Data Stats:', analytics.counts);
console.log('Error Summary:', analytics.errors);

// Find unused attributes
dcupl.quality.calculateUnusedData();
console.log('Unused:', analytics.unusedAttributes);

Track Event Flow

// Development: log all events
if (process.env.NODE_ENV === 'development') {
  dcupl.on((msg) => {
    console.log(`[dcupl] ${msg.type}`, {
      model: msg.modelKey,
      list: msg.listKey,
      duration: msg.duration ? `${msg.duration}ms` : undefined,
    });
  });
}

Integration with Error Tracking Services

Sentry Integration

import * as Sentry from '@sentry/browser';

const dcupl = new Dcupl({
  quality: { enabled: true },
  logging: {
    onLog: (entry) => {
      if (entry.level === 'error') {
        Sentry.captureException(new Error(entry.message), {
          tags: { module: entry.module },
          extra: entry.data,
        });
      }
    },
  },
});

// After init, report quality issues
await dcupl.init();
const errors = dcupl.quality.getErrors();

if (errors.length > 0) {
  Sentry.captureMessage('dcupl data quality issues', {
    level: 'warning',
    extra: { errorCount: errors.length, errors: errors.slice(0, 10) },
  });
}

Best Practices

1. Always Enable Quality Checking

// Always enable in development
const dcupl = new Dcupl({
  quality: { enabled: true },
});

2. Check Errors After Init

await dcupl.init();
const errors = dcupl.quality.getErrors();
// Don't ignore errors!

3. Use Event Listeners for Real-time Monitoring

dcupl.on((msg) => {
  if (msg.duration && msg.duration > 100) {
    reportSlowOperation(msg);
  }
});

4. Implement Graceful Fallbacks

try {
  await loader.config.fetch(remoteConfig);
} catch {
  loader.config.set(localFallback);
}

5. Log Appropriately per Environment

logging: {
  level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
  format: process.env.NODE_ENV === 'production' ? 'json' : 'pretty',
}