Implement proper error boundaries to prevent app crashes
Frontend Error Boundaries: - Created ErrorBoundary class for centralized error handling - Wraps critical functions (transcribe, translate, TTS) with error boundaries - Global error handlers for unhandled errors and promise rejections - Component-specific error recovery with fallback functions - User-friendly error notifications with auto-dismiss - Error logging to backend for monitoring - Prevents cascading failures from component errors Backend Error Handling: - Added error boundary decorator for Flask routes - Global Flask error handlers (404, 500, generic exceptions) - Frontend error logging endpoint (/api/log-error) - Structured error responses with component information - Full traceback logging for debugging - Production vs development error message handling Features: - Graceful degradation when components fail - Automatic error recovery attempts - Error history tracking (last 50 errors) - Component-specific error handling - Production error monitoring ready - Prevents full app crashes from isolated errors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		| @@ -16,18 +16,60 @@ import { | ||||
| } from './types'; | ||||
| import { TranslationCache } from './translationCache'; | ||||
| import { RequestQueueManager } from './requestQueue'; | ||||
| import { ErrorBoundary } from './errorBoundary'; | ||||
|  | ||||
| // Initialize error boundary | ||||
| const errorBoundary = ErrorBoundary.getInstance(); | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|     // Set up global error handler | ||||
|     errorBoundary.setGlobalErrorHandler((error, errorInfo) => { | ||||
|         console.error('Global error caught:', error); | ||||
|          | ||||
|         // Show user-friendly message based on component | ||||
|         if (errorInfo.component === 'transcription') { | ||||
|             const statusIndicator = document.getElementById('statusIndicator'); | ||||
|             if (statusIndicator) { | ||||
|                 statusIndicator.textContent = 'Transcription failed. Please try again.'; | ||||
|                 statusIndicator.classList.add('text-danger'); | ||||
|             } | ||||
|         } else if (errorInfo.component === 'translation') { | ||||
|             const translatedText = document.getElementById('translatedText'); | ||||
|             if (translatedText) { | ||||
|                 translatedText.innerHTML = '<p class="text-danger">Translation failed. Please try again.</p>'; | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Wrap initialization functions with error boundaries | ||||
|     const safeRegisterServiceWorker = errorBoundary.wrapAsync( | ||||
|         registerServiceWorker, | ||||
|         'service-worker', | ||||
|         async () => console.warn('Service worker registration failed, continuing without PWA features') | ||||
|     ); | ||||
|  | ||||
|     const safeInitApp = errorBoundary.wrap( | ||||
|         initApp, | ||||
|         'app-init', | ||||
|         () => console.error('App initialization failed') | ||||
|     ); | ||||
|  | ||||
|     const safeInitInstallPrompt = errorBoundary.wrap( | ||||
|         initInstallPrompt, | ||||
|         'install-prompt', | ||||
|         () => console.warn('Install prompt initialization failed') | ||||
|     ); | ||||
|  | ||||
|     // Register service worker | ||||
|     if ('serviceWorker' in navigator) { | ||||
|         registerServiceWorker(); | ||||
|         safeRegisterServiceWorker(); | ||||
|     } | ||||
|  | ||||
|     // Initialize app | ||||
|     initApp(); | ||||
|     safeInitApp(); | ||||
|  | ||||
|     // Check for PWA installation prompts | ||||
|     initInstallPrompt(); | ||||
|     safeInitInstallPrompt(); | ||||
| }); | ||||
|  | ||||
| // Service Worker Registration | ||||
| @@ -356,7 +398,7 @@ function initApp(): void { | ||||
|     } | ||||
|  | ||||
|     // Function to transcribe audio | ||||
|     async function transcribeAudio(audioBlob: Blob): Promise<void> { | ||||
|     const transcribeAudioBase = async function(audioBlob: Blob): Promise<void> { | ||||
|         const formData = new FormData(); | ||||
|         formData.append('audio', audioBlob, 'audio.webm'); // Add filename for better server handling | ||||
|         formData.append('source_lang', sourceLanguage.value); | ||||
| @@ -438,10 +480,24 @@ function initApp(): void { | ||||
|                 statusIndicator.textContent = 'Transcription failed'; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
|      | ||||
|     // Wrap transcribe function with error boundary | ||||
|     const transcribeAudio = errorBoundary.wrapAsync( | ||||
|         transcribeAudioBase, | ||||
|         'transcription', | ||||
|         async () => { | ||||
|             hideProgress(); | ||||
|             hideLoadingOverlay(); | ||||
|             sourceText.innerHTML = '<p class="text-danger">Transcription failed. Please try again.</p>'; | ||||
|             statusIndicator.textContent = 'Transcription error - please retry'; | ||||
|             statusIndicator.classList.remove('processing'); | ||||
|             statusIndicator.classList.add('error'); | ||||
|         } | ||||
|     ); | ||||
|  | ||||
|     // Translate button click event | ||||
|     translateBtn.addEventListener('click', async function() { | ||||
|     translateBtn.addEventListener('click', errorBoundary.wrapAsync(async function() { | ||||
|         if (!currentSourceText) { | ||||
|             return; | ||||
|         } | ||||
| @@ -556,7 +612,12 @@ function initApp(): void { | ||||
|                 statusIndicator.textContent = 'Translation failed'; | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|     }, 'translation', async () => { | ||||
|         hideProgress(); | ||||
|         hideLoadingOverlay(); | ||||
|         translatedText.innerHTML = '<p class="text-danger">Translation failed. Please try again.</p>'; | ||||
|         statusIndicator.textContent = 'Translation error - please retry'; | ||||
|     })); | ||||
|  | ||||
|     // Play source text | ||||
|     playSource.addEventListener('click', function() { | ||||
| @@ -575,7 +636,7 @@ function initApp(): void { | ||||
|     }); | ||||
|  | ||||
|     // Function to play audio via TTS | ||||
|     async function playAudio(text: string, language: string): Promise<void> { | ||||
|     const playAudioBase = async function(text: string, language: string): Promise<void> { | ||||
|         showProgress(); | ||||
|         showLoadingOverlay('Generating audio...'); | ||||
|          | ||||
| @@ -654,7 +715,19 @@ function initApp(): void { | ||||
|                 ttsServerMessage.textContent = 'Failed to connect to TTS server'; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
|      | ||||
|     // Wrap playAudio function with error boundary | ||||
|     const playAudio = errorBoundary.wrapAsync( | ||||
|         playAudioBase, | ||||
|         'tts', | ||||
|         async () => { | ||||
|             hideProgress(); | ||||
|             hideLoadingOverlay(); | ||||
|             statusIndicator.textContent = 'Audio playback failed'; | ||||
|             alert('Failed to generate audio. Please check your TTS server connection.'); | ||||
|         } | ||||
|     ); | ||||
|  | ||||
|     // Clear buttons | ||||
|     clearSource.addEventListener('click', function() { | ||||
|   | ||||
							
								
								
									
										286
									
								
								static/js/src/errorBoundary.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								static/js/src/errorBoundary.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,286 @@ | ||||
| // Error boundary implementation for better error handling | ||||
| export interface ErrorInfo { | ||||
|     message: string; | ||||
|     stack?: string; | ||||
|     component?: string; | ||||
|     timestamp: number; | ||||
|     userAgent: string; | ||||
|     url: string; | ||||
| } | ||||
|  | ||||
| export class ErrorBoundary { | ||||
|     private static instance: ErrorBoundary; | ||||
|     private errorLog: ErrorInfo[] = []; | ||||
|     private maxErrorLog = 50; | ||||
|     private errorHandlers: Map<string, (error: Error, errorInfo: ErrorInfo) => void> = new Map(); | ||||
|     private globalErrorHandler: ((error: Error, errorInfo: ErrorInfo) => void) | null = null; | ||||
|  | ||||
|     private constructor() { | ||||
|         this.setupGlobalErrorHandlers(); | ||||
|     } | ||||
|  | ||||
|     static getInstance(): ErrorBoundary { | ||||
|         if (!ErrorBoundary.instance) { | ||||
|             ErrorBoundary.instance = new ErrorBoundary(); | ||||
|         } | ||||
|         return ErrorBoundary.instance; | ||||
|     } | ||||
|  | ||||
|     private setupGlobalErrorHandlers(): void { | ||||
|         // Handle unhandled errors | ||||
|         window.addEventListener('error', (event: ErrorEvent) => { | ||||
|             const errorInfo: ErrorInfo = { | ||||
|                 message: event.message, | ||||
|                 stack: event.error?.stack, | ||||
|                 timestamp: Date.now(), | ||||
|                 userAgent: navigator.userAgent, | ||||
|                 url: window.location.href, | ||||
|                 component: 'global' | ||||
|             }; | ||||
|  | ||||
|             this.logError(event.error || new Error(event.message), errorInfo); | ||||
|             this.handleError(event.error || new Error(event.message), errorInfo); | ||||
|  | ||||
|             // Prevent default error handling | ||||
|             event.preventDefault(); | ||||
|         }); | ||||
|  | ||||
|         // Handle unhandled promise rejections | ||||
|         window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => { | ||||
|             const error = new Error(event.reason?.message || 'Unhandled Promise Rejection'); | ||||
|             const errorInfo: ErrorInfo = { | ||||
|                 message: error.message, | ||||
|                 stack: event.reason?.stack, | ||||
|                 timestamp: Date.now(), | ||||
|                 userAgent: navigator.userAgent, | ||||
|                 url: window.location.href, | ||||
|                 component: 'promise' | ||||
|             }; | ||||
|  | ||||
|             this.logError(error, errorInfo); | ||||
|             this.handleError(error, errorInfo); | ||||
|  | ||||
|             // Prevent default error handling | ||||
|             event.preventDefault(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Wrap a function with error boundary | ||||
|     wrap<T extends (...args: any[]) => any>( | ||||
|         fn: T, | ||||
|         component: string, | ||||
|         fallback?: (...args: Parameters<T>) => ReturnType<T> | ||||
|     ): T { | ||||
|         return ((...args: Parameters<T>): ReturnType<T> => { | ||||
|             try { | ||||
|                 const result = fn(...args); | ||||
|                  | ||||
|                 // Handle async functions | ||||
|                 if (result instanceof Promise) { | ||||
|                     return result.catch((error: Error) => { | ||||
|                         const errorInfo: ErrorInfo = { | ||||
|                             message: error.message, | ||||
|                             stack: error.stack, | ||||
|                             component, | ||||
|                             timestamp: Date.now(), | ||||
|                             userAgent: navigator.userAgent, | ||||
|                             url: window.location.href | ||||
|                         }; | ||||
|  | ||||
|                         this.logError(error, errorInfo); | ||||
|                         this.handleError(error, errorInfo); | ||||
|  | ||||
|                         if (fallback) { | ||||
|                             return fallback(...args) as ReturnType<T>; | ||||
|                         } | ||||
|                         throw error; | ||||
|                     }) as ReturnType<T>; | ||||
|                 } | ||||
|  | ||||
|                 return result; | ||||
|             } catch (error: any) { | ||||
|                 const errorInfo: ErrorInfo = { | ||||
|                     message: error.message, | ||||
|                     stack: error.stack, | ||||
|                     component, | ||||
|                     timestamp: Date.now(), | ||||
|                     userAgent: navigator.userAgent, | ||||
|                     url: window.location.href | ||||
|                 }; | ||||
|  | ||||
|                 this.logError(error, errorInfo); | ||||
|                 this.handleError(error, errorInfo); | ||||
|  | ||||
|                 if (fallback) { | ||||
|                     return fallback(...args); | ||||
|                 } | ||||
|                 throw error; | ||||
|             } | ||||
|         }) as T; | ||||
|     } | ||||
|  | ||||
|     // Wrap async functions specifically | ||||
|     wrapAsync<T extends (...args: any[]) => Promise<any>>( | ||||
|         fn: T, | ||||
|         component: string, | ||||
|         fallback?: (...args: Parameters<T>) => ReturnType<T> | ||||
|     ): T { | ||||
|         return (async (...args: Parameters<T>) => { | ||||
|             try { | ||||
|                 return await fn(...args); | ||||
|             } catch (error: any) { | ||||
|                 const errorInfo: ErrorInfo = { | ||||
|                     message: error.message, | ||||
|                     stack: error.stack, | ||||
|                     component, | ||||
|                     timestamp: Date.now(), | ||||
|                     userAgent: navigator.userAgent, | ||||
|                     url: window.location.href | ||||
|                 }; | ||||
|  | ||||
|                 this.logError(error, errorInfo); | ||||
|                 this.handleError(error, errorInfo); | ||||
|  | ||||
|                 if (fallback) { | ||||
|                     return fallback(...args); | ||||
|                 } | ||||
|                 throw error; | ||||
|             } | ||||
|         }) as T; | ||||
|     } | ||||
|  | ||||
|     // Register component-specific error handler | ||||
|     registerErrorHandler(component: string, handler: (error: Error, errorInfo: ErrorInfo) => void): void { | ||||
|         this.errorHandlers.set(component, handler); | ||||
|     } | ||||
|  | ||||
|     // Set global error handler | ||||
|     setGlobalErrorHandler(handler: (error: Error, errorInfo: ErrorInfo) => void): void { | ||||
|         this.globalErrorHandler = handler; | ||||
|     } | ||||
|  | ||||
|     private logError(error: Error, errorInfo: ErrorInfo): void { | ||||
|         // Add to error log | ||||
|         this.errorLog.push(errorInfo); | ||||
|  | ||||
|         // Keep only recent errors | ||||
|         if (this.errorLog.length > this.maxErrorLog) { | ||||
|             this.errorLog.shift(); | ||||
|         } | ||||
|  | ||||
|         // Log to console in development | ||||
|         console.error(`[${errorInfo.component}] Error:`, error); | ||||
|         console.error('Error Info:', errorInfo); | ||||
|  | ||||
|         // Send to monitoring service if available | ||||
|         this.sendToMonitoring(error, errorInfo); | ||||
|     } | ||||
|  | ||||
|     private handleError(error: Error, errorInfo: ErrorInfo): void { | ||||
|         // Check for component-specific handler | ||||
|         const componentHandler = this.errorHandlers.get(errorInfo.component || ''); | ||||
|         if (componentHandler) { | ||||
|             componentHandler(error, errorInfo); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Use global handler if set | ||||
|         if (this.globalErrorHandler) { | ||||
|             this.globalErrorHandler(error, errorInfo); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Default error handling | ||||
|         this.showErrorNotification(error, errorInfo); | ||||
|     } | ||||
|  | ||||
|     private showErrorNotification(error: Error, errorInfo: ErrorInfo): void { | ||||
|         // Create error notification | ||||
|         const notification = document.createElement('div'); | ||||
|         notification.className = 'alert alert-danger alert-dismissible fade show position-fixed bottom-0 end-0 m-3'; | ||||
|         notification.style.zIndex = '9999'; | ||||
|         notification.style.maxWidth = '400px'; | ||||
|          | ||||
|         const isUserFacing = this.isUserFacingError(error); | ||||
|         const message = isUserFacing ? error.message : 'An unexpected error occurred. Please try again.'; | ||||
|          | ||||
|         notification.innerHTML = ` | ||||
|             <strong><i class="fas fa-exclamation-circle"></i> Error${errorInfo.component ? ` in ${errorInfo.component}` : ''}</strong> | ||||
|             <p class="mb-0">${message}</p> | ||||
|             ${!isUserFacing ? '<small class="text-muted">The error has been logged for investigation.</small>' : ''} | ||||
|             <button type="button" class="btn-close" data-bs-dismiss="alert"></button> | ||||
|         `; | ||||
|  | ||||
|         document.body.appendChild(notification); | ||||
|  | ||||
|         // Auto-dismiss after 10 seconds | ||||
|         setTimeout(() => { | ||||
|             if (notification.parentNode) { | ||||
|                 notification.remove(); | ||||
|             } | ||||
|         }, 10000); | ||||
|     } | ||||
|  | ||||
|     private isUserFacingError(error: Error): boolean { | ||||
|         // Determine if error should be shown to user as-is | ||||
|         const userFacingMessages = [ | ||||
|             'rate limit', | ||||
|             'network', | ||||
|             'offline', | ||||
|             'not found', | ||||
|             'unauthorized', | ||||
|             'forbidden', | ||||
|             'timeout', | ||||
|             'invalid' | ||||
|         ]; | ||||
|  | ||||
|         const message = error.message.toLowerCase(); | ||||
|         return userFacingMessages.some(msg => message.includes(msg)); | ||||
|     } | ||||
|  | ||||
|     private async sendToMonitoring(error: Error, errorInfo: ErrorInfo): Promise<void> { | ||||
|         // Only send errors in production | ||||
|         if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             // Send error to backend monitoring endpoint | ||||
|             await fetch('/api/log-error', { | ||||
|                 method: 'POST', | ||||
|                 headers: { | ||||
|                     'Content-Type': 'application/json' | ||||
|                 }, | ||||
|                 body: JSON.stringify({ | ||||
|                     error: { | ||||
|                         message: error.message, | ||||
|                         stack: error.stack, | ||||
|                         name: error.name | ||||
|                     }, | ||||
|                     errorInfo | ||||
|                 }) | ||||
|             }); | ||||
|         } catch (monitoringError) { | ||||
|             // Fail silently - don't create error loop | ||||
|             console.error('Failed to send error to monitoring:', monitoringError); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Get error log for debugging | ||||
|     getErrorLog(): ErrorInfo[] { | ||||
|         return [...this.errorLog]; | ||||
|     } | ||||
|  | ||||
|     // Clear error log | ||||
|     clearErrorLog(): void { | ||||
|         this.errorLog = []; | ||||
|     } | ||||
|  | ||||
|     // Check if component has recent errors | ||||
|     hasRecentErrors(component: string, timeWindow: number = 60000): boolean { | ||||
|         const cutoff = Date.now() - timeWindow; | ||||
|         return this.errorLog.some( | ||||
|             error => error.component === component && error.timestamp > cutoff | ||||
|         ); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user