Skip to content
This repository was archived by the owner on Mar 8, 2026. It is now read-only.

Latest commit

 

History

History
1034 lines (868 loc) · 27.1 KB

File metadata and controls

1034 lines (868 loc) · 27.1 KB

PluginTemplate - Developer Guide

📚 Complete Guide to BrainDrive Plugin Error Handling

This guide provides comprehensive documentation for developers learning to implement robust error handling in BrainDrive plugins. The PluginTemplate serves as a working demonstration of all key error handling concepts and patterns.

🎯 Learning Objectives

After studying this plugin and guide, you will understand:

  1. Error Handling Architecture - How to structure comprehensive error handling
  2. Custom Error Types - Creating specific error classes for different scenarios
  3. Error Boundaries - React components that catch and handle component errors
  4. Service Integration - Error handling patterns for BrainDrive services
  5. User Experience - Providing meaningful error messages and recovery options
  6. Best Practices - Production-ready error handling techniques
  7. Common Pitfalls - What to avoid and how to debug error handling issues

🏗️ Architecture Overview

Error Handling Flow

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Error Occurs  │    │  Error Handler  │    │  Error Display  │
│                 │    │                 │    │                 │
│ 1. Custom Error │───▶│ 2. Strategy     │───▶│ 3. User-Friendly│
│    Types        │    │    Selection    │    │    Message      │
│                 │    │                 │    │                 │
│ 4. Error        │◀───│ 5. Recovery     │◀───│ 6. User Action  │
│    Recovery     │    │    Attempt      │    │    (Retry/etc)  │
└─────────────────┘    └─────────────────┘    └─────────────────┘

Key Components

  1. ErrorHandler Class - Centralized error handling with multiple strategies
  2. Custom Error Types - Specific error classes (PluginError, ServiceError, etc.)
  3. ErrorBoundary Component - React error boundary for component errors
  4. ErrorDisplay Component - Enhanced error display with user actions
  5. Error Utilities - Helper functions for error management

🔧 Implementation Guide

Step 1: Error Handler Integration

// In your component constructor (from PluginTemplate.tsx)
constructor(props: PluginTemplateProps) {
  super(props);
  
  // Initialize error handler with plugin context
  this.errorHandler = new ErrorHandler(
    {
      maxRetries: 3,
      retryDelay: 1000,
      enableLogging: true,
      enableReporting: true,
      userNotification: true,
      fallbackValues: {
        plugindata: null,
        theme: 'light',
        settings: {}
      }
    },
    {
      component: 'PluginTemplate',
      pluginId: props.pluginId || 'PluginTemplate',
      moduleId: props.moduleId || 'main'
    }
  );
  
  this.state = {
    isLoading: false,
    error: '',
    currentTheme: 'light',
    isInitializing: true,
    data: null,
    lastError: null,
    retryAvailable: false
  };
}

// In componentDidMount (from PluginTemplate.tsx)
async componentDidMount() {
  await this.errorHandler.safeAsync(
    async () => {
      await this.initializeServices();
      await this.loadInitialData();
      this.setState({ 
        isInitializing: false,
        error: '',
        lastError: null,
        retryAvailable: false
      });
    },
    undefined,
    ErrorStrategy.RETRY
  ).catch((error) => {
    this.handleComponentError(error, 'componentDidMount');
  });
}

Step 2: Custom Error Types

// Basic Plugin Error (from errorHandling.ts)
throw new PluginError(
  'Operation failed',
  'OPERATION_ERROR',
  { context: 'user-action' },
  true // recoverable
);

// Service Error for API/Service issues
throw new ServiceError(
  'API service unavailable',
  'api',
  'SERVICE_UNAVAILABLE',
  { endpoint: '/api/data' }
);

// Validation Error for input validation
throw new ValidationError(
  'Invalid email format',
  'email',
  'user@invalid'
);

// Network Error for API communication
throw new NetworkError(
  'Request timeout',
  408,
  '/api/plugin-data'
);

// Configuration Error for setup issues
throw new ConfigurationError(
  'Missing required configuration',
  'apiEndpoint'
);

Step 3: Service Error Handling

// Enhanced service initialization (from PluginTemplate.tsx)
private async initializeServices(): Promise<void> {
  const { services } = this.props;

  // Initialize theme service with error handling
  await this.errorHandler.safeAsync(async () => {
    if (services.theme) {
      const currentTheme = this.errorHandler.safeSync(
        () => services.theme!.getCurrentTheme(),
        'light'
      );
      this.setState({ currentTheme });

      // Listen for theme changes with error handling
      this.themeChangeListener = (theme: string) => {
        this.errorHandler.safeSync(() => {
          this.setState({ currentTheme: theme });
        });
      };
      
      services.theme.addThemeChangeListener(this.themeChangeListener);
      console.log('PluginTemplate: Theme service initialized successfully');
    } else {
      console.warn('PluginTemplate: Theme service not available');
    }
  }, undefined, ErrorStrategy.FALLBACK).catch(error => {
    throw new ServiceError(
      'Failed to initialize theme service',
      'theme',
      'THEME_INIT_ERROR',
      error
    );
  });

  // Initialize settings service with comprehensive error handling
  await this.errorHandler.safeAsync(async () => {
    if (services.settings) {
      try {
        const savedConfig = await services.settings.getSetting?.('plugin_template_config');
        if (savedConfig) {
          // Validate configuration before applying
          const validatedConfig = this.errorHandler.validate(
            savedConfig,
            [
              (config) => typeof config === 'object' || 'Configuration must be an object',
              (config) => config !== null || 'Configuration cannot be null'
            ],
            'plugin_template_config'
          );
          
          console.log('PluginTemplate: Loaded and validated saved config:', validatedConfig);
        }
        console.log('PluginTemplate: Settings service initialized successfully');
      } catch (error) {
        if (error instanceof ValidationError) {
          console.error('PluginTemplate: Invalid configuration:', error);
          // Use default configuration
        } else {
          throw new ServiceError(
            'Failed to load settings',
            'settings',
            'SETTINGS_LOAD_ERROR',
            error
          );
        }
      }
    } else {
      console.warn('PluginTemplate: Settings service not available');
    }
  }, undefined, ErrorStrategy.FALLBACK);
}

Step 4: Error Boundary Implementation

// Wrap your components with ErrorBoundary (from PluginTemplate.tsx)
render(): JSX.Element {
  const { currentTheme, isInitializing, error } = this.state;

  return (
    <ErrorBoundary
      onError={(error, errorInfo) => {
        console.error('PluginTemplate: React Error Boundary caught error:', error, errorInfo);
        // Additional error reporting can be added here
      }}
      resetOnPropsChange={true}
      resetKeys={[this.props.pluginId || 'unknown', this.props.moduleId || 'unknown']}
    >
      <div className={`plugin-template ${currentTheme === 'dark' ? 'dark-theme' : ''}`}>
        {isInitializing ? (
          this.renderLoading()
        ) : error ? (
          this.renderError()
        ) : (
          this.errorHandler.safeSync(
            () => this.renderContent(),
            <ErrorDisplay 
              error="Failed to render plugin content" 
              onRetry={this.handleRetry}
              variant="error"
            />
          )
        )}
      </div>
    </ErrorBoundary>
  );
}

📋 Error Types and Structure

PluginError (Base Class)

class PluginError extends Error {
  public readonly code: string;
  public readonly details?: any;
  public readonly timestamp: string;
  public readonly recoverable: boolean;

  constructor(
    message: string,
    code: string = 'PLUGIN_ERROR',
    details?: any,
    recoverable: boolean = true
  ) {
    super(message);
    this.name = 'PluginError';
    this.code = code;
    this.details = details;
    this.timestamp = new Date().toISOString();
    this.recoverable = recoverable;
  }

  toJSON() {
    return {
      name: this.name,
      message: this.message,
      code: this.code,
      details: this.details,
      timestamp: this.timestamp,
      recoverable: this.recoverable,
      stack: this.stack
    };
  }
}

ServiceError (Service-specific errors)

class ServiceError extends PluginError {
  public readonly service: string;

  constructor(
    message: string,
    service: string,
    code: string = 'SERVICE_ERROR',
    details?: any,
    recoverable: boolean = true
  ) {
    super(message, code, details, recoverable);
    this.name = 'ServiceError';
    this.service = service;
  }
}

ValidationError (Input validation errors)

class ValidationError extends PluginError {
  public readonly field?: string;
  public readonly value?: any;

  constructor(
    message: string,
    field?: string,
    value?: any,
    code: string = 'VALIDATION_ERROR'
  ) {
    super(message, code, { field, value }, false); // Not recoverable
    this.name = 'ValidationError';
    this.field = field;
    this.value = value;
  }
}

NetworkError (API/Network errors)

class NetworkError extends ServiceError {
  public readonly status?: number;
  public readonly url?: string;

  constructor(
    message: string,
    status?: number,
    url?: string,
    code: string = 'NETWORK_ERROR'
  ) {
    super(message, 'network', code, { status, url }, true);
    this.name = 'NetworkError';
    this.status = status;
    this.url = url;
  }
}

🎨 Error Handling Strategies

Strategy Types

enum ErrorStrategy {
  RETRY = 'retry',        // Retry the operation with exponential backoff
  FALLBACK = 'fallback',  // Use a fallback value or operation
  IGNORE = 'ignore',      // Log the error but continue execution
  ESCALATE = 'escalate',  // Report to monitoring systems
  USER_ACTION = 'user_action' // Require user intervention
}

Strategy Implementation

// Retry Strategy (from errorHandling.ts)
private async handleRetryStrategy(
  error: Error,
  currentCount: number
): Promise<{ handled: boolean; shouldRetry?: boolean }> {
  const maxRetries = this.config.maxRetries || 3;

  if (currentCount < maxRetries) {
    console.log(`🔄 Retrying operation (attempt ${currentCount + 1}/${maxRetries})`);
    
    // Wait before retry with exponential backoff
    if (this.config.retryDelay) {
      await this.delay(this.config.retryDelay * Math.pow(2, currentCount));
    }

    return { handled: true, shouldRetry: true };
  } else {
    console.error(`❌ Max retries (${maxRetries}) exceeded for error:`, error);
    return { handled: false, shouldRetry: false };
  }
}

// Fallback Strategy
private handleFallbackStrategy(error: Error): { handled: boolean; result?: any } {
  console.log('🔄 Using fallback strategy for error:', error.message);
  
  const fallbackKey = error.constructor.name.toLowerCase();
  const fallbackValue = this.config.fallbackValues?.[fallbackKey];

  return {
    handled: true,
    result: fallbackValue || this.getDefaultFallback(error)
  };
}

🎨 UI Components

Enhanced Error Display

// ErrorDisplay component with comprehensive features (from ErrorDisplay.tsx)
<ErrorDisplay
  error={errorInfo}
  onRetry={retryAvailable ? this.handleRetry : undefined}
  onDismiss={this.handleDismissError}
  showDetails={true}
  variant="error"
/>

// Error info structure
interface ErrorInfo {
  message: string;
  code?: string;
  details?: any;
  timestamp?: string;
  stack?: string;
}

// Component features:
// - Multiple variants (error, warning, info)
// - Retry and dismiss actions
// - Detailed error information toggle
// - Copy error details to clipboard
// - Responsive design with proper styling

Error Boundary Features

// ErrorBoundary component features (from ErrorBoundary.tsx)
<ErrorBoundary
  onError={(error, errorInfo) => {
    console.error('Component error:', error, errorInfo);
  }}
  resetOnPropsChange={true}
  resetKeys={[pluginId, moduleId]}
  fallback={<CustomErrorUI />}
>
  <YourComponent />
</ErrorBoundary>

// Features:
// - Automatic error catching and display
// - Reset functionality for error recovery
// - Custom fallback UI support
// - Error reporting integration
// - Development debugging information
// - Automatic retry mechanisms

🚨 Service Integration Patterns

API Service Error Handling

// Enhanced API service with error handling (from PluginService.ts)
async fetchData(): Promise<PluginData> {
  return this.errorHandler.safeAsync(async () => {
    // Validate API service availability
    if (!this.apiService) {
      throw new ServiceError(
        'API service not available',
        'api',
        'SERVICE_UNAVAILABLE',
        { method: 'fetchData' },
        false
      );
    }

    try {
      const response = await this.apiService.get('/api/plugin-template/data');
      
      // Validate response structure
      if (!response || typeof response !== 'object') {
        throw new NetworkError(
          'Invalid response format from API',
          (response as any)?.status,
          '/api/plugin-template/data'
        );
      }

      // Validate response data
      const data = response.data;
      if (!data) {
        throw new NetworkError(
          'No data received from API',
          response.status,
          '/api/plugin-template/data'
        );
      }

      // Validate data structure
      this.validateData(data);
      
      console.log('PluginService: Data fetched successfully');
      return data;

    } catch (error) {
      if (error instanceof PluginError) {
        throw error; // Re-throw our custom errors
      }

      // Handle network/API errors
      const networkError = new NetworkError(
        `Failed to fetch plugin data: ${error instanceof Error ? error.message : 'Unknown error'}`,
        undefined,
        '/api/plugin-template/data'
      );
      
      console.error('PluginService: API fetch failed:', networkError);
      throw networkError;
    }
  }, null, ErrorStrategy.RETRY);
}

Utility Functions with Error Handling

// Enhanced utility functions (from utils.ts)

// Safe JSON parsing with comprehensive error handling
export const safeJsonParse = <T = any>(jsonString: string, defaultValue: T): T => {
  try {
    // Validate input
    if (typeof jsonString !== 'string') {
      console.warn('safeJsonParse: Input is not a string:', typeof jsonString);
      return defaultValue;
    }

    if (jsonString.trim() === '') {
      console.warn('safeJsonParse: Input is empty string');
      return defaultValue;
    }

    const parsed = JSON.parse(jsonString);
    console.log('safeJsonParse: Successfully parsed JSON');
    return parsed;
  } catch (error) {
    console.warn('safeJsonParse: Failed to parse JSON:', {
      error: error instanceof Error ? error.message : error,
      input: jsonString.substring(0, 100) + (jsonString.length > 100 ? '...' : ''),
      inputLength: jsonString.length
    });
    return defaultValue;
  }
};

// Retry async operations with exponential backoff
export const retryAsync = async <T>(
  operation: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> => {
  let lastError: Error;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));
      
      if (attempt === maxRetries) {
        console.error(`retryAsync: All ${maxRetries + 1} attempts failed:`, lastError);
        throw lastError;
      }

      const delay = baseDelay * Math.pow(2, attempt);
      console.warn(`retryAsync: Attempt ${attempt + 1} failed, retrying in ${delay}ms:`, lastError.message);
      
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError!;
};

// Safe async operation with timeout
export const withTimeout = <T>(
  promise: Promise<T>,
  timeoutMs: number,
  timeoutMessage: string = 'Operation timed out'
): Promise<T> => {
  return Promise.race([
    promise,
    new Promise<never>((_, reject) => {
      setTimeout(() => {
        reject(new Error(timeoutMessage));
      }, timeoutMs);
    })
  ]);
};

🔍 Error Handling Best Practices

1. Use Appropriate Error Types

// ✅ Good - Specific error type with context
throw new ValidationError(
  'Email address is required',
  'email',
  userInput.email
);

// ❌ Bad - Generic error without context
throw new Error('Validation failed');

2. Provide Rich Error Context

// ✅ Good - Rich context for debugging
throw new ServiceError(
  'Failed to load user preferences',
  'settings',
  'LOAD_ERROR',
  { 
    userId: user.id, 
    attemptCount: 3,
    lastAttempt: new Date().toISOString()
  }
);

// ❌ Bad - No context
throw new Error('Load failed');

3. Handle Errors at the Right Level

// ✅ Good - Handle at component level with user feedback
class MyComponent extends React.Component {
  async componentDidMount() {
    try {
      await this.loadData();
    } catch (error) {
      this.handleComponentError(error, 'componentDidMount');
    }
  }

  private handleComponentError = (error: unknown, context: string) => {
    const normalizedError = ErrorUtils.normalizeError(error);
    const pluginError = new PluginError(
      `Component error in ${context}: ${normalizedError.message}`,
      'COMPONENT_ERROR',
      { context, originalError: normalizedError },
      true
    );

    this.setState({
      error: ErrorUtils.getUserMessage(pluginError),
      lastError: pluginError,
      retryAvailable: true
    });
  };
}

4. Use Error Boundaries for Component Protection

// ✅ Good - Wrap components in error boundaries
function App() {
  return (
    <ErrorBoundary>
      <PluginContent />
    </ErrorBoundary>
  );
}

// ❌ Bad - No error boundary protection
function App() {
  return <PluginContent />;
}

5. Provide User-Friendly Messages

// ✅ Good - User-friendly messages
const getUserMessage = (error: Error): string => {
  if (error instanceof ValidationError) {
    return `Please check your input: ${error.message}`;
  }
  if (error instanceof NetworkError) {
    return 'Network connection issue. Please try again.';
  }
  if (error instanceof ServiceError) {
    return `Service temporarily unavailable: ${error.service}`;
  }
  return 'An unexpected error occurred. Please try again.';
};

// ❌ Bad - Technical error messages
const message = error.message; // "XMLHttpRequest failed with status 500"

🧪 Testing Error Handling

Unit Tests

describe('Error Handling', () => {
  let errorHandler: ErrorHandler;

  beforeEach(() => {
    errorHandler = new ErrorHandler({
      maxRetries: 3,
      retryDelay: 100,
      enableLogging: false,
      enableReporting: false
    });
  });

  it('should retry failed operations', async () => {
    const mockOperation = jest.fn()
      .mockRejectedValueOnce(new Error('First failure'))
      .mockRejectedValueOnce(new Error('Second failure'))
      .mockResolvedValueOnce('success');

    const result = await errorHandler.safeAsync(
      mockOperation,
      'fallback',
      ErrorStrategy.RETRY
    );

    expect(result).toBe('success');
    expect(mockOperation).toHaveBeenCalledTimes(3);
  });

  it('should use fallback after max retries exceeded', async () => {
    const mockOperation = jest.fn().mockRejectedValue(new Error('Always fails'));

    const result = await errorHandler.safeAsync(
      mockOperation,
      'fallback',
      ErrorStrategy.FALLBACK
    );

    expect(result).toBe('fallback');
  });

  it('should validate input correctly', () => {
    const validators = [
      (value: string) => value.length > 0 || 'Value cannot be empty',
      (value: string) => value.includes('@') || 'Must contain @'
    ];

    expect(() => {
      errorHandler.validate('invalid-email', validators, 'email');
    }).toThrow(ValidationError);
  });
});

Component Testing

describe('Component Error Handling', () => {
  it('should display error boundary on component error', () => {
    const ThrowError = () => {
      throw new Error('Test error');
    };

    const { getByText } = render(
      <ErrorBoundary>
        <ThrowError />
      </ErrorBoundary>
    );

    expect(getByText(/Component Error/)).toBeInTheDocument();
  });

  it('should show retry button for recoverable errors', () => {
    const recoverableError = new PluginError('Test error', 'TEST', {}, true);
    
    const { getByText } = render(
      <ErrorDisplay 
        error={recoverableError.toJSON()} 
        onRetry={jest.fn()}
      />
    );

    expect(getByText('Try Again')).toBeInTheDocument();
  });
});

🔍 Debugging and Monitoring

Error Statistics

// Get error statistics for monitoring
const errorStats = errorHandler.getErrorStats();
console.log('Error statistics:', errorStats);
// Output: { "NetworkError:timeout": 3, "ValidationError:email": 1 }

// Reset error counts for testing
errorHandler.resetErrorCounts();

Development Debugging

// Enhanced error display in development mode
{process.env.NODE_ENV === 'development' && (
  <div style={{
    marginTop: '12px',
    padding: '8px',
    backgroundColor: '#f8f9fa',
    borderRadius: '4px',
    fontSize: '11px',
    color: '#6c757d'
  }}>
    <strong>🔧 Debug Info:</strong>
    <ul style={{ margin: '4px 0', paddingLeft: '16px' }}>
      <li>Retry Count: {this.retryCount}/{this.maxRetries}</li>
      <li>Error Handler Stats: {JSON.stringify(this.errorHandler.getErrorStats())}</li>
      <li>Component State: {JSON.stringify({ 
        isLoading: this.state.isLoading, 
        isInitializing: this.state.isInitializing 
      })}</li>
    </ul>
  </div>
)}

Error Reporting Integration

// Configure error reporting for production
const errorHandler = new ErrorHandler({
  enableReporting: true,
  // Add your error reporting service integration
});

// Example integration with external service
const reportError = async (error: Error) => {
  if (process.env.NODE_ENV === 'production') {
    await errorReportingService.report({
      error: error.message,
      stack: error.stack,
      context: getErrorContext(),
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
      url: window.location.href
    });
  }
};

🚨 Common Pitfalls

1. Silent Failures

// ❌ Bad - Silent failure
async loadData() {
  try {
    await api.getData();
  } catch (error) {
    // Silent failure - user doesn't know what happened
  }
}

// ✅ Good - Proper error handling
async loadData() {
  try {
    await api.getData();
  } catch (error) {
    this.handleComponentError(error, 'loadData');
  }
}

2. Generic Error Messages

// ❌ Bad - Generic error
throw new Error('Something went wrong');

// ✅ Good - Specific error with context
throw new ServiceError(
  'Failed to connect to user service',
  'userService',
  'CONNECTION_ERROR',
  { endpoint: '/api/users', timeout: 5000 }
);

3. Not Using Error Boundaries

// ❌ Bad - No error boundary
function App() {
  return <ComplexComponent />;
}

// ✅ Good - Protected with error boundary
function App() {
  return (
    <ErrorBoundary>
      <ComplexComponent />
    </ErrorBoundary>
  );
}

4. Improper Error Recovery

// ❌ Bad - No recovery mechanism
catch (error) {
  console.error(error);
  // User is stuck with broken UI
}

// ✅ Good - Provide recovery options
catch (error) {
  this.setState({
    error: ErrorUtils.getUserMessage(error),
    retryAvailable: ErrorUtils.isRecoverable(error)
  });
}

📚 Code Examples

Complete Error Handling Flow

// Example of complete error handling implementation
class DataComponent extends React.Component {
  private errorHandler = new ErrorHandler();

  async componentDidMount() {
    await this.errorHandler.safeAsync(
      async () => {
        const data = await this.fetchData();
        this.setState({ data, error: null });
      },
      undefined,
      ErrorStrategy.RETRY
    ).catch(error => {
      this.handleComponentError(error, 'componentDidMount');
    });
  }

  private async fetchData() {
    if (!this.props.services.api) {
      throw new ServiceError('API service not available', 'api');
    }

    const response = await this.props.services.api.get('/data');
    
    if (!response.data) {
      throw new NetworkError('No data received', response.status);
    }

    return response.data;
  }

  private handleComponentError = (error: unknown, context: string) => {
    const normalizedError = ErrorUtils.normalizeError(error);
    this.setState({
      error: ErrorUtils.getUserMessage(normalizedError),
      retryAvailable: ErrorUtils.isRecoverable(normalizedError)
    });
  };

  render() {
    if (this.state.error) {
      return (
        <ErrorDisplay
          error={this.state.error}
          onRetry={this.state.retryAvailable ? this.handleRetry : undefined}
          showDetails={process.env.NODE_ENV === 'development'}
        />
      );
    }

    return <DataDisplay data={this.state.data} />;
  }
}

🎓 Next Steps

After mastering the error handling patterns in this template:

  1. Customize Error Types - Create plugin-specific error classes
  2. Implement Monitoring - Add error reporting and analytics
  3. Enhance User Experience - Create custom error recovery flows
  4. Add Testing - Write comprehensive error handling tests
  5. Performance Optimization - Optimize error handling for production

💡 Tips for Success

  1. Start Simple - Begin with basic error handling and gradually add complexity
  2. Test Error Scenarios - Regularly test error conditions and recovery paths
  3. Monitor in Production - Use error reporting to identify real-world issues
  4. User-Centric Design - Always consider the user experience during errors
  5. Document Error Codes - Maintain clear documentation of error types and codes
  6. Regular Reviews - Periodically review and update error handling strategies

🔗 Related Resources


Remember: Good error handling is not just about catching errors—it's about providing a great user experience even when things go wrong. Use this template as a foundation and customize it for your specific plugin needs.