Skip to main content

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

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.