Back to Qiankun

Error Handling

docs/cookbook/error-handling.md

2.10.1626.9 KB
Original Source

Error Handling

Robust error handling is essential for micro-frontend applications where multiple independent applications run within the same context. This guide covers comprehensive strategies for handling errors, implementing graceful degradation, and maintaining application stability across qiankun-based micro-frontend systems.

🎯 Error Types in Micro-Frontends

Common Error Categories

Micro-frontend applications face unique error scenarios:

  • Loading Errors: Failed to fetch or parse micro application resources
  • Runtime Errors: JavaScript errors within micro applications
  • Communication Errors: Failed inter-application communication
  • Network Errors: API calls and resource loading failures
  • Sandbox Errors: Issues with JavaScript and CSS isolation
  • Lifecycle Errors: Problems during mount/unmount processes
  • Version Conflicts: Dependency version mismatches

Error Impact Assessment

javascript
// Error severity levels for micro-frontend applications
const ERROR_LEVELS = {
  CRITICAL: 'critical',    // Main app or core functionality affected
  HIGH: 'high',           // Major micro app functionality lost
  MEDIUM: 'medium',       // Partial micro app functionality affected
  LOW: 'low',            // Minor features or visual issues
  INFO: 'info'           // Non-blocking informational issues
};

const ErrorClassifier = {
  classify(error, appName, context) {
    // Critical: Main app crashes or core navigation fails
    if (appName === 'main' || context.includes('navigation')) {
      return ERROR_LEVELS.CRITICAL;
    }
    
    // High: User cannot complete primary workflows
    if (context.includes('checkout') || context.includes('auth')) {
      return ERROR_LEVELS.HIGH;
    }
    
    // Medium: Feature degradation but app still usable
    if (error.name === 'ChunkLoadError' || error.name === 'TypeError') {
      return ERROR_LEVELS.MEDIUM;
    }
    
    // Default to low for other errors
    return ERROR_LEVELS.LOW;
  }
};

🛡️ qiankun Error Boundaries

Global Error Handling

Set up global error handlers for the entire micro-frontend ecosystem:

javascript
import { addGlobalUncaughtErrorHandler, removeGlobalUncaughtErrorHandler } from 'qiankun';

// Global error handler for all micro apps
const globalErrorHandler = (event) => {
  const { error, appName, lifecycleName } = event;
  
  console.error(`Error in micro app "${appName}" during "${lifecycleName}":`, error);
  
  // Report to error tracking service
  reportError({
    error,
    appName,
    lifecycle: lifecycleName,
    timestamp: Date.now(),
    userAgent: navigator.userAgent,
    url: window.location.href
  });
  
  // Implement recovery strategy
  handleMicroAppError(appName, error, lifecycleName);
};

// Register global error handler
addGlobalUncaughtErrorHandler(globalErrorHandler);

// Remove when cleaning up (e.g., in app unmount)
// removeGlobalUncaughtErrorHandler(globalErrorHandler);

Lifecycle-Specific Error Handling

javascript
// Error handling in lifecycle hooks
const errorHandlingLifecycles = {
  async beforeLoad(app) {
    try {
      // Pre-loading checks
      const healthCheck = await fetch(`${app.entry}/health`);
      if (!healthCheck.ok) {
        throw new Error(`Health check failed for ${app.name}`);
      }
    } catch (error) {
      console.warn(`Pre-load health check failed for ${app.name}:`, error);
      // Continue with loading but flag as potentially unstable
      markAppAsUnstable(app.name);
    }
  },

  async beforeMount(app) {
    try {
      // Validate app requirements
      validateAppRequirements(app);
    } catch (error) {
      // Attempt to fix common issues
      await attemptAutoFix(app, error);
    }
  },

  async afterMount(app) {
    // Verify successful mount
    setTimeout(() => {
      const container = document.querySelector(app.container);
      if (!container || container.children.length === 0) {
        console.error(`Mount verification failed for ${app.name}`);
        showFallbackContent(app.container, app.name);
      }
    }, 1000);
  },

  async beforeUnmount(app) {
    try {
      // Clean up resources
      cleanupAppResources(app.name);
    } catch (error) {
      console.warn(`Cleanup error for ${app.name}:`, error);
      // Force cleanup
      forceCleanup(app.name);
    }
  }
};

🚨 Framework-Specific Error Boundaries

React Error Boundaries

jsx
// React error boundary for micro applications
import React from 'react';

class MicroAppErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
      retryCount: 0,
      lastRetry: null
    };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error,
      errorInfo
    });

    // Report error
    this.reportError(error, errorInfo);

    // Attempt automatic recovery
    this.attemptRecovery(error);
  }

  reportError = (error, errorInfo) => {
    const errorReport = {
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack
      },
      errorInfo,
      appName: this.props.appName,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent,
      retryCount: this.state.retryCount
    };

    // Send to error tracking service
    fetch('/api/errors', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(errorReport)
    }).catch(err => console.error('Failed to report error:', err));
  };

  attemptRecovery = (error) => {
    const { retryCount, lastRetry } = this.state;
    const now = Date.now();
    
    // Prevent too frequent retries
    if (lastRetry && now - lastRetry < 5000) {
      return;
    }
    
    // Limit retry attempts
    if (retryCount >= 3) {
      console.error(`Max retry attempts reached for ${this.props.appName}`);
      return;
    }

    setTimeout(() => {
      this.setState({
        hasError: false,
        error: null,
        errorInfo: null,
        retryCount: retryCount + 1,
        lastRetry: now
      });
    }, 2000 * Math.pow(2, retryCount)); // Exponential backoff
  };

  render() {
    if (this.state.hasError) {
      const { appName, fallbackComponent: FallbackComponent } = this.props;
      
      if (FallbackComponent) {
        return (
          <FallbackComponent
            error={this.state.error}
            appName={appName}
            onRetry={() => this.attemptRecovery(this.state.error)}
          />
        );
      }

      return (
        <div className="micro-app-error">
          <h3>Application Error</h3>
          <p>The {appName} application encountered an error.</p>
          <button onClick={() => this.attemptRecovery(this.state.error)}>
            Retry ({this.state.retryCount}/3)
          </button>
          <details style={{ marginTop: '1rem' }}>
            <summary>Error Details</summary>
            <pre>{this.state.error?.stack}</pre>
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

// Usage with micro app
function MicroAppContainer({ appName, entry }) {
  return (
    <MicroAppErrorBoundary 
      appName={appName}
      fallbackComponent={CustomErrorFallback}
    >
      <div id={`${appName}-container`} />
    </MicroAppErrorBoundary>
  );
}

Vue Error Handling

javascript
// Vue global error handler for micro apps
const app = createApp(MainApp);

app.config.errorHandler = (err, instance, info) => {
  const appName = instance?.$root?.$options?.name || 'unknown';
  
  console.error(`Vue error in ${appName}:`, err, info);
  
  // Report error
  reportVueError({
    error: err,
    appName,
    info,
    timestamp: Date.now()
  });
  
  // Attempt recovery
  if (instance && typeof instance.$forceUpdate === 'function') {
    instance.$forceUpdate();
  }
};

// Vue 2 error boundary component
Vue.component('ErrorBoundary', {
  data() {
    return {
      hasError: false,
      error: null
    };
  },
  
  errorCaptured(err, instance, info) {
    this.hasError = true;
    this.error = err;
    
    // Report error
    this.reportError(err, info);
    
    // Prevent error from propagating
    return false;
  },
  
  methods: {
    reportError(error, info) {
      // Error reporting logic
    },
    
    retry() {
      this.hasError = false;
      this.error = null;
      this.$forceUpdate();
    }
  },
  
  render(h) {
    if (this.hasError) {
      return h('div', { class: 'error-boundary' }, [
        h('h3', 'Something went wrong'),
        h('button', { on: { click: this.retry } }, 'Retry'),
        h('pre', this.error?.message)
      ]);
    }
    
    return this.$slots.default;
  }
});

🔄 Graceful Degradation Strategies

Progressive Enhancement

javascript
// Progressive feature loading with fallbacks
class FeatureLoader {
  constructor() {
    this.features = new Map();
    this.fallbacks = new Map();
  }
  
  register(featureName, loader, fallback) {
    this.features.set(featureName, loader);
    this.fallbacks.set(featureName, fallback);
  }
  
  async load(featureName) {
    try {
      const loader = this.features.get(featureName);
      if (!loader) {
        throw new Error(`Feature "${featureName}" not registered`);
      }
      
      const feature = await loader();
      return feature;
      
    } catch (error) {
      console.warn(`Failed to load feature "${featureName}":`, error);
      
      const fallback = this.fallbacks.get(featureName);
      if (fallback) {
        return await fallback();
      }
      
      throw error;
    }
  }
}

// Usage example
const featureLoader = new FeatureLoader();

// Register advanced dashboard with fallback
featureLoader.register(
  'advanced-dashboard',
  () => import('./AdvancedDashboard'),
  () => import('./BasicDashboard')
);

// Register chart component with static fallback
featureLoader.register(
  'interactive-charts',
  () => import('./InteractiveCharts'),
  () => Promise.resolve(() => '<div>Charts unavailable</div>')
);

Fallback UI Components

jsx
// Comprehensive fallback components
const ErrorFallbacks = {
  // Network error fallback
  NetworkError: ({ onRetry, appName }) => (
    <div className="error-fallback network-error">
      <div className="error-icon">🌐</div>
      <h3>Connection Problem</h3>
      <p>Unable to load {appName}. Please check your internet connection.</p>
      <div className="error-actions">
        <button onClick={onRetry} className="retry-button">
          Try Again
        </button>
        <button onClick={() => window.location.reload()}>
          Refresh Page
        </button>
      </div>
    </div>
  ),

  // JavaScript error fallback
  JavaScriptError: ({ error, appName, onRetry }) => (
    <div className="error-fallback js-error">
      <div className="error-icon">⚠️</div>
      <h3>Application Error</h3>
      <p>The {appName} application encountered a technical issue.</p>
      <div className="error-actions">
        <button onClick={onRetry} className="retry-button">
          Reload Application
        </button>
        <button onClick={() => reportIssue(error, appName)}>
          Report Issue
        </button>
      </div>
      {process.env.NODE_ENV === 'development' && (
        <details className="error-details">
          <summary>Technical Details</summary>
          <pre>{error.stack}</pre>
        </details>
      )}
    </div>
  ),

  // Loading timeout fallback
  LoadingTimeout: ({ appName, onRetry }) => (
    <div className="error-fallback loading-timeout">
      <div className="error-icon">⏱️</div>
      <h3>Loading Timeout</h3>
      <p>{appName} is taking longer than expected to load.</p>
      <div className="error-actions">
        <button onClick={onRetry} className="retry-button">
          Try Again
        </button>
        <button onClick={() => loadBasicVersion(appName)}>
          Load Basic Version
        </button>
      </div>
    </div>
  ),

  // Generic fallback
  Generic: ({ error, appName, onRetry }) => (
    <div className="error-fallback generic">
      <div className="error-icon">🔧</div>
      <h3>Temporary Issue</h3>
      <p>We're experiencing technical difficulties with {appName}.</p>
      <div className="error-actions">
        <button onClick={onRetry} className="retry-button">
          Try Again
        </button>
      </div>
    </div>
  )
};

Circuit Breaker Pattern

javascript
// Circuit breaker for micro app loading
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000, monitor = 30000) {
    this.failureThreshold = threshold;
    this.timeout = timeout;
    this.monitoringPeriod = monitor;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.nextAttemptTime = null;
  }

  async execute(operation, appName) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttemptTime) {
        throw new Error(`Circuit breaker is OPEN for ${appName}`);
      }
      this.state = 'HALF_OPEN';
    }

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

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

  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttemptTime = Date.now() + this.timeout;
    }
  }

  getState() {
    return this.state;
  }
}

// Usage with micro app loading
const circuitBreakers = new Map();

const loadMicroAppWithCircuitBreaker = async (appConfig) => {
  const { name } = appConfig;
  
  if (!circuitBreakers.has(name)) {
    circuitBreakers.set(name, new CircuitBreaker());
  }
  
  const breaker = circuitBreakers.get(name);
  
  try {
    return await breaker.execute(() => loadMicroApp(appConfig), name);
  } catch (error) {
    console.error(`Circuit breaker prevented loading ${name}:`, error);
    throw error;
  }
};

📊 Error Monitoring and Reporting

Comprehensive Error Tracking

javascript
// Advanced error tracking system
class ErrorTracker {
  constructor(config) {
    this.config = {
      endpoint: '/api/errors',
      batchSize: 10,
      batchTimeout: 5000,
      maxRetries: 3,
      ...config
    };
    
    this.errorQueue = [];
    this.batchTimeout = null;
    this.retryCount = new Map();
  }

  track(error, context = {}) {
    const errorData = this.serializeError(error, context);
    
    // Add to queue
    this.errorQueue.push(errorData);
    
    // Process batch if queue is full
    if (this.errorQueue.length >= this.config.batchSize) {
      this.processBatch();
    } else {
      // Set timeout for batch processing
      this.scheduleBatchProcessing();
    }
  }

  serializeError(error, context) {
    return {
      id: this.generateErrorId(),
      timestamp: Date.now(),
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack,
        fileName: error.fileName,
        lineNumber: error.lineNumber,
        columnNumber: error.columnNumber
      },
      context: {
        appName: context.appName || 'unknown',
        userId: context.userId,
        sessionId: this.getSessionId(),
        url: window.location.href,
        userAgent: navigator.userAgent,
        viewport: {
          width: window.innerWidth,
          height: window.innerHeight
        },
        ...context
      },
      environment: {
        isDevelopment: process.env.NODE_ENV === 'development',
        timestamp: Date.now(),
        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
      }
    };
  }

  scheduleBatchProcessing() {
    if (this.batchTimeout) {
      clearTimeout(this.batchTimeout);
    }
    
    this.batchTimeout = setTimeout(() => {
      this.processBatch();
    }, this.config.batchTimeout);
  }

  async processBatch() {
    if (this.errorQueue.length === 0) return;
    
    const batch = this.errorQueue.splice(0, this.config.batchSize);
    
    try {
      await this.sendErrors(batch);
      
      // Clear retry count on success
      batch.forEach(error => {
        this.retryCount.delete(error.id);
      });
      
    } catch (error) {
      console.error('Failed to send error batch:', error);
      
      // Retry logic
      batch.forEach(errorData => {
        const retries = this.retryCount.get(errorData.id) || 0;
        if (retries < this.config.maxRetries) {
          this.retryCount.set(errorData.id, retries + 1);
          this.errorQueue.unshift(errorData); // Add back to front of queue
        }
      });
    }
  }

  async sendErrors(errors) {
    const response = await fetch(this.config.endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ errors })
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return response.json();
  }

  generateErrorId() {
    return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  getSessionId() {
    // Implementation to get/generate session ID
    return sessionStorage.getItem('sessionId') || 'anonymous';
  }
}

// Initialize global error tracker
const errorTracker = new ErrorTracker();

// Track unhandled errors
window.addEventListener('error', (event) => {
  errorTracker.track(event.error, {
    type: 'unhandled_error',
    source: 'window.onerror'
  });
});

// Track unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
  errorTracker.track(event.reason, {
    type: 'unhandled_rejection',
    source: 'unhandledrejection'
  });
});

Performance Impact Monitoring

javascript
// Monitor error impact on performance
class ErrorImpactMonitor {
  constructor() {
    this.errorImpacts = new Map();
    this.performanceBaseline = this.measureBaseline();
  }

  measureBaseline() {
    return {
      loadTime: performance.now(),
      memoryUsage: performance.memory ? performance.memory.usedJSHeapSize : 0,
      timing: performance.timing
    };
  }

  recordErrorImpact(errorId, appName) {
    const impact = {
      errorId,
      appName,
      timestamp: Date.now(),
      performance: {
        loadTime: performance.now(),
        memoryUsage: performance.memory ? performance.memory.usedJSHeapSize : 0,
        timing: performance.timing
      },
      userExperience: {
        pageVisible: !document.hidden,
        userActive: this.isUserActive(),
        scrollPosition: window.scrollY
      }
    };

    this.errorImpacts.set(errorId, impact);
    this.analyzeImpact(impact);
  }

  analyzeImpact(impact) {
    const { performance: current } = impact;
    const baseline = this.performanceBaseline;

    const memoryIncrease = current.memoryUsage - baseline.memoryUsage;
    const loadTimeIncrease = current.loadTime - baseline.loadTime;

    if (memoryIncrease > 50 * 1024 * 1024) { // 50MB
      console.warn('High memory impact detected after error:', impact);
    }

    if (loadTimeIncrease > 5000) { // 5 seconds
      console.warn('Significant performance degradation after error:', impact);
    }
  }

  isUserActive() {
    // Simple user activity detection
    return Date.now() - this.lastUserActivity < 30000;
  }
}

🔧 Recovery Mechanisms

Automatic Recovery Strategies

javascript
// Comprehensive recovery system
class RecoveryManager {
  constructor() {
    this.recoveryStrategies = new Map();
    this.setupDefaultStrategies();
  }

  setupDefaultStrategies() {
    // Network error recovery
    this.register('NetworkError', async (error, context) => {
      await this.waitForConnection();
      return this.reloadMicroApp(context.appName);
    });

    // Chunk load error recovery
    this.register('ChunkLoadError', async (error, context) => {
      // Clear webpack cache
      if (window.__webpack_require__ && window.__webpack_require__.cache) {
        delete window.__webpack_require__.cache[error.request];
      }
      
      // Reload with cache busting
      return this.reloadWithCacheBust(context.appName);
    });

    // Script error recovery
    this.register('TypeError', async (error, context) => {
      // Attempt to reload dependencies
      await this.reloadDependencies(context.appName);
      return this.remountMicroApp(context.appName);
    });

    // Memory error recovery
    this.register('RangeError', async (error, context) => {
      // Force garbage collection
      if (window.gc) window.gc();
      
      // Reduce memory footprint
      await this.reducememoryFootprint(context.appName);
      return this.reloadMicroApp(context.appName);
    });
  }

  register(errorType, strategy) {
    this.recoveryStrategies.set(errorType, strategy);
  }

  async recover(error, context) {
    const strategy = this.recoveryStrategies.get(error.name);
    
    if (strategy) {
      try {
        console.log(`Attempting recovery for ${error.name} in ${context.appName}`);
        const result = await strategy(error, context);
        console.log(`Recovery successful for ${context.appName}`);
        return result;
      } catch (recoveryError) {
        console.error(`Recovery failed for ${context.appName}:`, recoveryError);
        return this.fallbackRecovery(context);
      }
    }
    
    return this.fallbackRecovery(context);
  }

  async waitForConnection() {
    return new Promise((resolve) => {
      if (navigator.onLine) {
        resolve();
      } else {
        const handleOnline = () => {
          window.removeEventListener('online', handleOnline);
          resolve();
        };
        window.addEventListener('online', handleOnline);
      }
    });
  }

  async reloadMicroApp(appName) {
    // Unmount current instance
    try {
      await unmountMicroApp(appName);
    } catch (error) {
      console.warn(`Failed to unmount ${appName}:`, error);
    }

    // Reload the micro app
    const appConfig = getAppConfig(appName);
    return loadMicroApp(appConfig);
  }

  async reloadWithCacheBust(appName) {
    const appConfig = getAppConfig(appName);
    const cacheBustEntry = `${appConfig.entry}?t=${Date.now()}`;
    
    return loadMicroApp({
      ...appConfig,
      entry: cacheBustEntry
    });
  }

  async fallbackRecovery(context) {
    console.log(`Using fallback recovery for ${context.appName}`);
    
    // Show fallback UI
    showFallbackUI(context.appName);
    
    // Report recovery failure
    reportRecoveryFailure(context);
    
    return null;
  }
}

User-Initiated Recovery

jsx
// User-controlled recovery interface
const RecoveryPanel = ({ appName, error, onRecover, onDismiss }) => {
  const [recovering, setRecovering] = useState(false);
  const [lastAttempt, setLastAttempt] = useState(null);

  const handleRecover = async (strategy) => {
    setRecovering(true);
    setLastAttempt(Date.now());
    
    try {
      await onRecover(strategy);
    } catch (error) {
      console.error('User-initiated recovery failed:', error);
    } finally {
      setRecovering(false);
    }
  };

  const recoveryOptions = [
    {
      key: 'reload',
      label: 'Reload Application',
      description: 'Restart the application from scratch',
      action: () => handleRecover('reload')
    },
    {
      key: 'reset',
      label: 'Reset to Default',
      description: 'Clear all data and reload',
      action: () => handleRecover('reset')
    },
    {
      key: 'safe-mode',
      label: 'Safe Mode',
      description: 'Load with minimal features',
      action: () => handleRecover('safe-mode')
    }
  ];

  return (
    <div className="recovery-panel">
      <div className="recovery-header">
        <h3>Recovery Options for {appName}</h3>
        <button onClick={onDismiss} className="close-button">×</button>
      </div>
      
      <div className="error-summary">
        <p><strong>Error:</strong> {error.message}</p>
        {lastAttempt && (
          <p><small>Last attempt: {new Date(lastAttempt).toLocaleTimeString()}</small></p>
        )}
      </div>
      
      <div className="recovery-options">
        {recoveryOptions.map(option => (
          <button
            key={option.key}
            onClick={option.action}
            disabled={recovering}
            className="recovery-option"
          >
            <div className="option-label">{option.label}</div>
            <div className="option-description">{option.description}</div>
          </button>
        ))}
      </div>
      
      {recovering && (
        <div className="recovery-progress">
          <div className="spinner" />
          <span>Attempting recovery...</span>
        </div>
      )}
    </div>
  );
};

🎯 Best Practices Summary

✅ Error Handling Do's

  1. Implement global error handlers for comprehensive coverage
  2. Use error boundaries in each micro application
  3. Provide meaningful error messages for users
  4. Implement graceful degradation with fallback UIs
  5. Monitor and track errors systematically
  6. Test error scenarios during development
  7. Implement automatic recovery where possible
  8. Clear error context in reports
  9. Handle network failures gracefully
  10. Provide user recovery options

❌ Error Handling Don'ts

  1. Don't ignore errors or fail silently
  2. Don't show technical details to end users
  3. Don't retry indefinitely without limits
  4. Don't block the entire application for one micro app error
  5. Don't forget to clean up after errors
  6. Don't rely solely on automatic recovery
  7. Don't overwhelm users with error messages
  8. Don't forget about memory leaks in error scenarios
  9. Don't skip error testing in production-like environments
  10. Don't ignore user feedback about errors

🔄 Error Recovery Checklist

javascript
// Comprehensive error handling checklist
const errorHandlingChecklist = {
  prevention: {
    validation: '✓ Input validation implemented',
    typeChecking: '✓ TypeScript or PropTypes used',
    testing: '✓ Error scenarios tested',
    monitoring: '✓ Health checks in place'
  },
  
  detection: {
    globalHandlers: '✓ Global error handlers set up',
    boundaries: '✓ Error boundaries implemented',
    logging: '✓ Comprehensive error logging',
    alerting: '✓ Real-time error alerts'
  },
  
  recovery: {
    gracefulDegradation: '✓ Fallback UIs implemented',
    automaticRecovery: '✓ Auto-recovery strategies',
    userRecovery: '✓ User-initiated recovery options',
    resourceCleanup: '✓ Proper cleanup on errors'
  },
  
  learning: {
    errorTracking: '✓ Error analytics in place',
    trendAnalysis: '✓ Error trend monitoring',
    rootCauseAnalysis: '✓ RCA process defined',
    continuousImprovement: '✓ Regular error review meetings'
  }
};