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:
- Error Handling Architecture - How to structure comprehensive error handling
- Custom Error Types - Creating specific error classes for different scenarios
- Error Boundaries - React components that catch and handle component errors
- Service Integration - Error handling patterns for BrainDrive services
- User Experience - Providing meaningful error messages and recovery options
- Best Practices - Production-ready error handling techniques
- 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β
- ErrorHandler Class - Centralized error handling with multiple strategies
- Custom Error Types - Specific error classes (PluginError, ServiceError, etc.)
- ErrorBoundary Component - React error boundary for component errors
- ErrorDisplay Component - Enhanced error display with user actions
- 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:
- Customize Error Types - Create plugin-specific error classes
- Implement Monitoring - Add error reporting and analytics
- Enhance User Experience - Create custom error recovery flows
- Add Testing - Write comprehensive error handling tests
- Performance Optimization - Optimize error handling for production
π‘ Tips for Successβ
- Start Simple - Begin with basic error handling and gradually add complexity
- Test Error Scenarios - Regularly test error conditions and recovery paths
- Monitor in Production - Use error reporting to identify real-world issues
- User-Centric Design - Always consider the user experience during errors
- Document Error Codes - Maintain clear documentation of error types and codes
- Regular Reviews - Periodically review and update error handling strategies
π Related Resourcesβ
- React Error Boundaries
- JavaScript Error Handling
- TypeScript Error Handling
- Error Handling Guide - Detailed technical reference
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.