Error Handling Guide
Comprehensive error handling patterns for the DDEX Suite, covering all types of errors from validation failures to system-level issues.
Problem Statement
DDEX processing involves complex XML parsing, validation against multiple schemas, and integration with external systems. Errors can occur at multiple levels:
- Parse Errors: Invalid XML, unsupported DDEX versions, malformed structure
- Validation Errors: Schema violations, missing required fields, invalid references
- System Errors: Memory exhaustion, file I/O failures, network timeouts
- Business Logic Errors: Duplicate releases, invalid metadata combinations
- Integration Errors: DSP API failures, database constraint violations
Proper error handling ensures robust production systems and provides clear feedback for debugging and resolution.
Solution Approach
The DDEX Suite provides a comprehensive error hierarchy with structured error information, context preservation, and recovery strategies. This guide demonstrates patterns for handling each error type effectively.
Error Hierarchy Overview
TypeScript Error Types
// Base error types
interface DDEXError {
  code: string;
  message: string;
  context?: Record<string, any>;
  cause?: Error;
  severity: 'fatal' | 'error' | 'warning' | 'info';
}
// Specific error types
interface ParseError extends DDEXError {
  line?: number;
  column?: number;
  xmlPath?: string;
}
interface ValidationError extends DDEXError {
  field: string;
  value?: any;
  expectedType?: string;
  validationRule: string;
}
interface SystemError extends DDEXError {
  systemCode: string;
  retryable: boolean;
  retryAfter?: number;
}
Python Error Types
from typing import Optional, Dict, Any, List
from enum import Enum
class ErrorSeverity(Enum):
    FATAL = "fatal"
    ERROR = "error"
    WARNING = "warning"
    INFO = "info"
class DDEXError(Exception):
    def __init__(
        self,
        message: str,
        code: str,
        context: Optional[Dict[str, Any]] = None,
        severity: ErrorSeverity = ErrorSeverity.ERROR,
        cause: Optional[Exception] = None
    ):
        super().__init__(message)
        self.code = code
        self.context = context or {}
        self.severity = severity
        self.cause = cause
class ParseError(DDEXError):
    def __init__(
        self,
        message: str,
        code: str = "PARSE_ERROR",
        line: Optional[int] = None,
        column: Optional[int] = None,
        xml_path: Optional[str] = None,
        **kwargs
    ):
        super().__init__(message, code, **kwargs)
        self.line = line
        self.column = column
        self.xml_path = xml_path
Parse Error Handling
Basic Parse Error Recovery
import { DdexParser, ParseError } from 'ddex-parser';
async function robustParse(xmlContent: string) {
  const parser = new DdexParser();
  
  try {
    return await parser.parse(xmlContent);
  } catch (error) {
    if (error instanceof ParseError) {
      // Handle specific parse errors
      switch (error.code) {
        case 'INVALID_XML':
          console.error(`XML is malformed at line ${error.line}, column ${error.column}`);
          // Attempt XML repair or preprocessing
          return await attemptXmlRepair(xmlContent, error);
          
        case 'UNSUPPORTED_VERSION':
          console.error(`DDEX version ${error.context?.version} not supported`);
          // Try version detection and conversion
          return await convertToSupportedVersion(xmlContent);
          
        case 'SCHEMA_VIOLATION':
          console.error(`Schema validation failed: ${error.message}`);
          // Log for manual review
          await logForReview(xmlContent, error);
          throw error;
          
        default:
          console.error(`Unknown parse error: ${error.code}`);
          throw error;
      }
    }
    
    // Re-throw non-parse errors
    throw error;
  }
}
async function attemptXmlRepair(xmlContent: string, error: ParseError): Promise<any> {
  console.log('Attempting XML repair...');
  
  // Common repairs
  let repairedXml = xmlContent
    .replace(/&(?![a-zA-Z]+;)/g, '&')  // Fix unescaped ampersands
    .replace(/<!\[CDATA\[.*?\]\]>/gs, (match) => {
      // Ensure CDATA sections are properly closed
      return match.endsWith(']]>') ? match : match + ']]>';
    });
  
  // Try parsing the repaired XML
  const parser = new DdexParser();
  try {
    return await parser.parse(repairedXml);
  } catch (repairError) {
    console.error('XML repair failed, original error was more informative');
    throw error; // Throw original error
  }
}
Python Parse Error Handling
from ddex_parser import DdexParser, ParseError, ValidationError
import logging
from typing import Optional
async def robust_parse(xml_content: str) -> Optional[dict]:
    parser = DdexParser()
    
    try:
        return await parser.parse(xml_content)
    except ParseError as e:
        logging.error(f"Parse error {e.code}: {e.message}")
        
        if e.code == "INVALID_XML":
            # Attempt repair
            repaired = await attempt_xml_repair(xml_content, e)
            if repaired:
                return repaired
                
        elif e.code == "UNSUPPORTED_VERSION":
            # Version conversion
            converted = await convert_version(xml_content, e.context.get("version"))
            if converted:
                return await parser.parse(converted)
                
        # Log for manual review
        await log_parse_failure(xml_content, e)
        return None
        
    except Exception as e:
        logging.error(f"Unexpected error during parsing: {e}")
        raise
async def attempt_xml_repair(xml_content: str, error: ParseError) -> Optional[dict]:
    """Attempt common XML repairs"""
    import re
    
    # Fix common issues
    repaired = xml_content
    
    # Fix encoding issues
    repaired = re.sub(r'[^\x09\x0A\x0D\x20-\x7E]', '', repaired)
    
    # Fix unescaped characters
    repaired = repaired.replace('&', '&').replace('&amp;', '&')
    repaired = repaired.replace('<', '<').replace('<?', '<?').replace('</', '</')
    
    # Try parsing repaired XML
    parser = DdexParser()
    try:
        return await parser.parse(repaired)
    except Exception:
        return None
Validation Error Handling
Comprehensive Validation with Error Collection
import { DdexBuilder, ValidationError, ValidationResult } from 'ddex-builder';
interface ValidationReport {
  isValid: boolean;
  errors: ValidationError[];
  warnings: ValidationError[];
  summary: {
    totalErrors: number;
    criticalErrors: number;
    fixableErrors: number;
  };
}
async function validateWithReport(buildRequest: any): Promise<ValidationReport> {
  const builder = new DdexBuilder();
  const errors: ValidationError[] = [];
  const warnings: ValidationError[] = [];
  
  try {
    // Enable comprehensive validation
    const result = await builder.validate(buildRequest, {
      validateReferences: true,
      checkBusinessRules: true,
      validateMetadata: true,
      allowPartialValidation: true
    });
    
    return {
      isValid: true,
      errors: [],
      warnings: [],
      summary: { totalErrors: 0, criticalErrors: 0, fixableErrors: 0 }
    };
    
  } catch (error) {
    if (error instanceof ValidationError) {
      // Collect all validation errors
      const allErrors = error.validationErrors || [error];
      
      for (const validationError of allErrors) {
        if (validationError.severity === 'fatal' || validationError.severity === 'error') {
          errors.push(validationError);
        } else {
          warnings.push(validationError);
        }
      }
      
      return {
        isValid: false,
        errors,
        warnings,
        summary: {
          totalErrors: errors.length + warnings.length,
          criticalErrors: errors.filter(e => e.severity === 'fatal').length,
          fixableErrors: errors.filter(e => isFixableError(e)).length
        }
      };
    }
    
    throw error;
  }
}
function isFixableError(error: ValidationError): boolean {
  const fixableCodes = [
    'MISSING_REQUIRED_FIELD',
    'INVALID_FORMAT',
    'DUPLICATE_REFERENCE',
    'INVALID_DATE_FORMAT'
  ];
  
  return fixableCodes.includes(error.code);
}
async function autoFixValidationErrors(
  buildRequest: any, 
  errors: ValidationError[]
): Promise<{ fixed: any; remainingErrors: ValidationError[] }> {
  let fixed = JSON.parse(JSON.stringify(buildRequest)); // Deep copy
  const remainingErrors: ValidationError[] = [];
  
  for (const error of errors) {
    try {
      switch (error.code) {
        case 'MISSING_REQUIRED_FIELD':
          fixed = await addMissingField(fixed, error);
          break;
          
        case 'INVALID_DATE_FORMAT':
          fixed = await fixDateFormat(fixed, error);
          break;
          
        case 'DUPLICATE_REFERENCE':
          fixed = await removeDuplicateReference(fixed, error);
          break;
          
        default:
          remainingErrors.push(error);
      }
    } catch (fixError) {
      console.warn(`Failed to auto-fix error ${error.code}: ${fixError.message}`);
      remainingErrors.push(error);
    }
  }
  
  return { fixed, remainingErrors };
}
Python Validation Error Handling
from dataclasses import dataclass
from typing import List, Dict, Any, Optional
from ddex_builder import DdexBuilder, ValidationError
@dataclass
class ValidationReport:
    is_valid: bool
    errors: List[ValidationError]
    warnings: List[ValidationError]
    fixable_errors: List[ValidationError]
    critical_count: int
async def validate_with_comprehensive_report(build_request: Dict[str, Any]) -> ValidationReport:
    builder = DdexBuilder()
    errors = []
    warnings = []
    
    try:
        await builder.validate(
            build_request,
            validate_references=True,
            check_business_rules=True,
            validate_metadata=True
        )
        
        return ValidationReport(
            is_valid=True,
            errors=[],
            warnings=[],
            fixable_errors=[],
            critical_count=0
        )
        
    except ValidationError as e:
        # Collect all validation errors
        all_errors = getattr(e, 'validation_errors', [e])
        
        for error in all_errors:
            if error.severity in ['fatal', 'error']:
                errors.append(error)
            else:
                warnings.append(error)
        
        fixable_errors = [e for e in errors if is_fixable_error(e)]
        critical_count = len([e for e in errors if e.severity == 'fatal'])
        
        return ValidationReport(
            is_valid=False,
            errors=errors,
            warnings=warnings,
            fixable_errors=fixable_errors,
            critical_count=critical_count
        )
def is_fixable_error(error: ValidationError) -> bool:
    fixable_codes = {
        'MISSING_REQUIRED_FIELD',
        'INVALID_FORMAT',
        'DUPLICATE_REFERENCE',
        'INVALID_DATE_FORMAT'
    }
    return error.code in fixable_codes
async def auto_fix_validation_errors(
    build_request: Dict[str, Any],
    errors: List[ValidationError]
) -> Dict[str, Any]:
    """Auto-fix common validation errors"""
    import copy
    fixed = copy.deepcopy(build_request)
    
    for error in errors:
        try:
            if error.code == 'MISSING_REQUIRED_FIELD':
                fixed = await add_missing_field(fixed, error)
            elif error.code == 'INVALID_DATE_FORMAT':
                fixed = await fix_date_format(fixed, error)
            elif error.code == 'DUPLICATE_REFERENCE':
                fixed = await remove_duplicate_reference(fixed, error)
        except Exception as fix_error:
            logging.warning(f"Failed to auto-fix {error.code}: {fix_error}")
    
    return fixed
System Error Handling
Retry Strategies and Circuit Breakers
import { DdexParser, SystemError } from 'ddex-parser';
class RetryConfig {
  maxRetries: number = 3;
  baseDelay: number = 1000; // ms
  maxDelay: number = 10000; // ms
  exponentialBackoff: boolean = true;
  retryableErrors: string[] = [
    'NETWORK_TIMEOUT',
    'MEMORY_PRESSURE',
    'TEMPORARY_UNAVAILABLE'
  ];
}
class CircuitBreaker {
  private failures = 0;
  private lastFailureTime = 0;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
  
  constructor(
    private threshold: number = 5,
    private timeout: number = 60000 // 1 minute
  ) {}
  
  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime < this.timeout) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
    }
    
    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }
  
  private onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    
    if (this.failures >= this.threshold) {
      this.state = 'OPEN';
    }
  }
}
async function parseWithRetry(
  xmlContent: string,
  config: RetryConfig = new RetryConfig()
): Promise<any> {
  const circuitBreaker = new CircuitBreaker();
  
  return await circuitBreaker.execute(async () => {
    return await retryOperation(
      () => new DdexParser().parse(xmlContent),
      config
    );
  });
}
async function retryOperation<T>(
  operation: () => Promise<T>,
  config: RetryConfig
): Promise<T> {
  let lastError: Error;
  
  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      
      if (attempt === config.maxRetries) {
        break; // Final attempt failed
      }
      
      if (error instanceof SystemError && !config.retryableErrors.includes(error.code)) {
        throw error; // Non-retryable error
      }
      
      const delay = config.exponentialBackoff
        ? Math.min(config.baseDelay * Math.pow(2, attempt), config.maxDelay)
        : config.baseDelay;
      
      console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}
Resource Management and Cleanup
class ResourceManager {
  private resources: Set<any> = new Set();
  
  async withResource<T>(
    resourceFactory: () => Promise<any>,
    operation: (resource: any) => Promise<T>
  ): Promise<T> {
    let resource: any;
    
    try {
      resource = await resourceFactory();
      this.resources.add(resource);
      
      return await operation(resource);
    } catch (error) {
      console.error('Operation failed, cleaning up resources:', error);
      throw error;
    } finally {
      if (resource) {
        await this.cleanup(resource);
      }
    }
  }
  
  private async cleanup(resource: any) {
    try {
      this.resources.delete(resource);
      
      if (resource.close) {
        await resource.close();
      } else if (resource.destroy) {
        await resource.destroy();
      }
    } catch (cleanupError) {
      console.error('Cleanup failed:', cleanupError);
    }
  }
  
  async cleanupAll() {
    const promises = Array.from(this.resources).map(resource => this.cleanup(resource));
    await Promise.allSettled(promises);
  }
}
// Usage example
async function processLargeFile(filePath: string) {
  const resourceManager = new ResourceManager();
  
  try {
    return await resourceManager.withResource(
      () => createStreamingParser({ maxMemory: '500MB' }),
      async (parser) => {
        return await parser.parseFile(filePath);
      }
    );
  } catch (error) {
    console.error(`Failed to process ${filePath}:`, error);
    throw error;
  } finally {
    await resourceManager.cleanupAll();
  }
}
Business Logic Error Handling
Duplicate Detection and Resolution
interface DuplicateStrategy {
  onDuplicateRelease: 'error' | 'skip' | 'merge' | 'replace';
  onDuplicateResource: 'error' | 'skip' | 'merge' | 'replace';
  mergeStrategy?: 'newest' | 'manual' | 'custom';
}
class BusinessRuleValidator {
  constructor(private strategy: DuplicateStrategy) {}
  
  async validateReleases(releases: any[]): Promise<any[]> {
    const releaseMap = new Map<string, any[]>();
    
    // Group by identifier
    for (const release of releases) {
      const key = this.getReleaseKey(release);
      if (!releaseMap.has(key)) {
        releaseMap.set(key, []);
      }
      releaseMap.get(key)!.push(release);
    }
    
    const validatedReleases: any[] = [];
    
    for (const [key, duplicates] of releaseMap) {
      if (duplicates.length === 1) {
        validatedReleases.push(duplicates[0]);
        continue;
      }
      
      // Handle duplicates
      switch (this.strategy.onDuplicateRelease) {
        case 'error':
          throw new Error(`Duplicate release found: ${key}`);
          
        case 'skip':
          console.warn(`Skipping duplicate releases for: ${key}`);
          validatedReleases.push(duplicates[0]);
          break;
          
        case 'merge':
          const merged = await this.mergeReleases(duplicates);
          validatedReleases.push(merged);
          break;
          
        case 'replace':
          const latest = this.getLatestRelease(duplicates);
          validatedReleases.push(latest);
          break;
      }
    }
    
    return validatedReleases;
  }
  
  private getReleaseKey(release: any): string {
    // Create unique key from ICPN, UPC, or other identifiers
    return [
      release.icpn,
      release.catalogNumber,
      release.title
    ].filter(Boolean).join('|');
  }
  
  private async mergeReleases(releases: any[]): Promise<any> {
    const merged = { ...releases[0] };
    
    for (let i = 1; i < releases.length; i++) {
      const release = releases[i];
      
      // Merge resources
      merged.resources = [
        ...merged.resources,
        ...release.resources.filter((r: any) => 
          !merged.resources.some((mr: any) => mr.id === r.id)
        )
      ];
      
      // Update metadata with latest values
      if (release.updatedAt > merged.updatedAt) {
        merged.title = release.title || merged.title;
        merged.updatedAt = release.updatedAt;
      }
    }
    
    return merged;
  }
}
Integration Error Handling
DSP API Error Handling
interface DSPConfig {
  baseUrl: string;
  apiKey: string;
  timeout: number;
  retries: number;
}
class DSPIntegrationError extends Error {
  constructor(
    public dsp: string,
    public statusCode: number,
    public response: any,
    message: string
  ) {
    super(message);
  }
}
class DSPClient {
  constructor(private config: DSPConfig) {}
  
  async submitRelease(ddexXml: string): Promise<{ id: string; status: string }> {
    try {
      const response = await this.makeRequest('/releases', {
        method: 'POST',
        body: ddexXml,
        headers: { 'Content-Type': 'application/xml' }
      });
      
      return response.json();
    } catch (error) {
      throw this.handleDSPError(error);
    }
  }
  
  private async makeRequest(endpoint: string, options: any): Promise<Response> {
    const url = `${this.config.baseUrl}${endpoint}`;
    
    const response = await fetch(url, {
      ...options,
      timeout: this.config.timeout,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${this.config.apiKey}`
      }
    });
    
    if (!response.ok) {
      const errorBody = await response.text();
      throw new DSPIntegrationError(
        'spotify',
        response.status,
        errorBody,
        `HTTP ${response.status}: ${response.statusText}`
      );
    }
    
    return response;
  }
  
  private handleDSPError(error: any): Error {
    if (error instanceof DSPIntegrationError) {
      switch (error.statusCode) {
        case 400:
          return new Error(`Invalid DDEX submission: ${error.response}`);
        case 401:
          return new Error('Authentication failed - check API key');
        case 429:
          return new Error('Rate limit exceeded - retry after delay');
        case 500:
          return new Error('DSP server error - retry later');
        default:
          return error;
      }
    }
    
    return error;
  }
}
Error Monitoring and Alerting
Structured Logging and Metrics
import { createLogger, format, transports } from 'winston';
interface ErrorMetrics {
  errorCount: number;
  errorsByType: Map<string, number>;
  errorsByTime: Map<string, number>;
  lastError?: Error;
}
class ErrorTracker {
  private metrics: ErrorMetrics = {
    errorCount: 0,
    errorsByType: new Map(),
    errorsByTime: new Map()
  };
  
  private logger = createLogger({
    format: format.combine(
      format.timestamp(),
      format.errors({ stack: true }),
      format.json()
    ),
    transports: [
      new transports.File({ filename: 'error.log', level: 'error' }),
      new transports.File({ filename: 'combined.log' })
    ]
  });
  
  trackError(error: Error, context?: any) {
    this.metrics.errorCount++;
    this.metrics.lastError = error;
    
    // Track by type
    const errorType = error.constructor.name;
    this.metrics.errorsByType.set(
      errorType,
      (this.metrics.errorsByType.get(errorType) || 0) + 1
    );
    
    // Track by time (hourly buckets)
    const hourKey = new Date().toISOString().substring(0, 13);
    this.metrics.errorsByTime.set(
      hourKey,
      (this.metrics.errorsByTime.get(hourKey) || 0) + 1
    );
    
    // Log with context
    this.logger.error('DDEX processing error', {
      error: {
        message: error.message,
        stack: error.stack,
        type: errorType
      },
      context,
      metrics: this.getMetricsSummary()
    });
    
    // Check for alerts
    this.checkAlerts();
  }
  
  private checkAlerts() {
    const recentErrors = this.getRecentErrorCount(60 * 1000); // Last minute
    
    if (recentErrors > 10) {
      this.sendAlert('High error rate detected', {
        count: recentErrors,
        period: '1 minute'
      });
    }
  }
  
  private getRecentErrorCount(timeWindowMs: number): number {
    const cutoff = new Date(Date.now() - timeWindowMs);
    const cutoffHour = cutoff.toISOString().substring(0, 13);
    
    return this.metrics.errorsByTime.get(cutoffHour) || 0;
  }
  
  private async sendAlert(message: string, data: any) {
    // Integration with monitoring systems (Slack, PagerDuty, etc.)
    console.error(`ALERT: ${message}`, data);
  }
  
  getMetricsSummary() {
    return {
      totalErrors: this.metrics.errorCount,
      errorTypes: Object.fromEntries(this.metrics.errorsByType),
      lastErrorTime: this.metrics.lastError ? new Date().toISOString() : null
    };
  }
}
// Global error tracker instance
export const errorTracker = new ErrorTracker();
// Global error handler
process.on('uncaughtException', (error) => {
  errorTracker.trackError(error, { type: 'uncaught_exception' });
  process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
  const error = reason instanceof Error ? reason : new Error(String(reason));
  errorTracker.trackError(error, { type: 'unhandled_rejection', promise });
});
Common Pitfalls and Solutions
Memory Leaks in Error Handling
Pitfall: Accumulating error objects in memory without cleanup
// DON'T - Memory leak
class BadErrorHandler {
  private allErrors: Error[] = []; // Never cleaned up
  
  handleError(error: Error) {
    this.allErrors.push(error); // Memory grows indefinitely
  }
}
// DO - Bounded error tracking
class GoodErrorHandler {
  private recentErrors: Error[] = [];
  private maxErrors = 100;
  
  handleError(error: Error) {
    this.recentErrors.push(error);
    
    // Keep only recent errors
    if (this.recentErrors.length > this.maxErrors) {
      this.recentErrors = this.recentErrors.slice(-this.maxErrors);
    }
  }
}
Error Swallowing
Pitfall: Catching errors without proper handling or logging
// DON'T - Silent failures
try {
  await processFile(file);
} catch (error) {
  // Silent failure - error is lost
}
// DO - Explicit error handling
try {
  await processFile(file);
} catch (error) {
  errorTracker.trackError(error, { file: file.name });
  
  if (error instanceof SystemError && error.retryable) {
    await retryProcessing(file);
  } else {
    throw error; // Re-throw if not recoverable
  }
}
Infinite Retry Loops
Pitfall: Retrying non-retryable errors indefinitely
// DON'T - Infinite retries
async function badRetry(operation: () => Promise<any>) {
  while (true) {
    try {
      return await operation();
    } catch (error) {
      await delay(1000); // Retry forever
    }
  }
}
// DO - Bounded retries with backoff
async function goodRetry(operation: () => Promise<any>) {
  const maxRetries = 3;
  let delay = 1000;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await operation();
    } catch (error) {
      if (i === maxRetries - 1 || !isRetryableError(error)) {
        throw error;
      }
      
      await sleep(delay);
      delay *= 2; // Exponential backoff
    }
  }
}
Performance Considerations
- Error Object Creation: Create error objects lazily to avoid performance overhead
- Stack Trace Generation: Disable stack traces in production for non-critical errors
- Logging Overhead: Use asynchronous logging to avoid blocking operations
- Memory Management: Implement error object pooling for high-frequency errors
- Context Collection: Gather error context efficiently without deep object copying
Links to API Documentation
- Parser API Reference
- Builder API Reference
- Error Types Documentation
- Python Error Handling
- Validation API
This comprehensive error handling guide ensures robust DDEX processing with proper error recovery, monitoring, and debugging capabilities across all supported platforms.