@orchard9ai/error-handling
Version:
Federated error handling package with go-core-http-toolkit format support and logging integration
429 lines (370 loc) • 11.3 kB
text/typescript
import { ErrorHandler } from '../core/ErrorHandler.js';
import { parseUnknownError } from '../utils/errorParsers.js';
import type { ErrorContext, DisplayError, ErrorHandlerConfig } from '../types/index.js';
/**
* Global error handler configuration
*/
export interface GlobalErrorHandlerConfig {
/** Whether to enable global error handling */
enabled?: boolean;
/** Whether to show toast notifications for errors */
showToasts?: boolean;
/** Whether to log errors to console */
logToConsole?: boolean;
/** Custom error handler callback */
onError?: (error: Error, displayError: DisplayError, context: ErrorContext) => void;
/** Custom unhandled rejection handler */
onUnhandledRejection?: (reason: any, promise: Promise<any>, displayError: DisplayError) => void;
/** Error handler configuration */
errorHandlerConfig?: Partial<ErrorHandlerConfig>;
/** Errors to ignore (by message or error type) */
ignoredErrors?: string[];
/** Maximum number of errors to handle per minute (rate limiting) */
maxErrorsPerMinute?: number;
}
/**
* Error statistics for monitoring
*/
export interface ErrorStats {
totalErrors: number;
errorsByType: Record<string, number>;
errorsByMinute: number[];
lastError?: {
message: string;
timestamp: Date;
context?: ErrorContext;
};
}
/**
* Global error handler for catching unhandled errors
*/
export class GlobalErrorHandler {
private config: GlobalErrorHandlerConfig & {
enabled: boolean;
showToasts: boolean;
logToConsole: boolean;
errorHandlerConfig: Partial<ErrorHandlerConfig>;
ignoredErrors: string[];
maxErrorsPerMinute: number;
};
private errorHandler: ErrorHandler;
private errorCounts: number[];
private stats: ErrorStats;
private originalErrorHandler?: OnErrorEventHandler | null;
private originalRejectionHandler?: ((this: WindowEventHandlers, ev: PromiseRejectionEvent) => any) | null;
private cleanupInterval?: number;
constructor(config: GlobalErrorHandlerConfig = {}) {
this.config = {
enabled: true,
showToasts: true,
logToConsole: true,
onError: config.onError,
onUnhandledRejection: config.onUnhandledRejection,
errorHandlerConfig: {},
ignoredErrors: [
'Script error.',
'Non-Error promise rejection captured',
'ResizeObserver loop limit exceeded'
],
maxErrorsPerMinute: 10,
...config
};
this.errorHandler = new ErrorHandler({
loggerName: 'global-error-handler',
...this.config.errorHandlerConfig
});
this.errorCounts = [];
this.stats = {
totalErrors: 0,
errorsByType: {},
errorsByMinute: []
};
if (this.config.enabled) {
this.install();
}
}
/**
* Install global error handlers
*/
install(): void {
if (typeof window === 'undefined') {
return; // Server-side rendering
}
// Store original handlers
this.originalErrorHandler = window.onerror;
this.originalRejectionHandler = window.onunhandledrejection;
// Install window error handler
window.onerror = (message, source, lineno, colno, error) => {
this.handleWindowError(message, source, lineno, colno, error);
// Call original handler if it exists
if (this.originalErrorHandler) {
return this.originalErrorHandler(message, source, lineno, colno, error);
}
return false;
};
// Install unhandled promise rejection handler
window.onunhandledrejection = (event) => {
this.handleUnhandledRejection(event);
// Call original handler if it exists
if (this.originalRejectionHandler) {
return this.originalRejectionHandler.call(window, event);
}
};
// Clean up error counts every minute
this.cleanupInterval = window.setInterval(() => {
this.cleanupErrorCounts();
}, 60000);
}
/**
* Uninstall global error handlers
*/
uninstall(): void {
if (typeof window === 'undefined') {
return;
}
// Clear interval
if (this.cleanupInterval) {
window.clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
// Restore original handlers
window.onerror = this.originalErrorHandler || null;
window.onunhandledrejection = this.originalRejectionHandler || null;
this.originalErrorHandler = null;
this.originalRejectionHandler = null;
}
/**
* Handle window errors
*/
private handleWindowError(
message: string | Event,
source?: string,
lineno?: number,
colno?: number,
error?: Error
): void {
// Check rate limiting
if (!this.checkRateLimit()) {
return;
}
// Check if error should be ignored
const errorMessage = typeof message === 'string' ? message : error?.message || 'Unknown error';
if (this.shouldIgnoreError(errorMessage)) {
return;
}
// Create context
const context: ErrorContext = {
component: 'global-error-handler',
action: 'window-error',
metadata: {
source,
lineno,
colno,
userAgent: navigator.userAgent,
url: window.location.href
}
};
// Handle the error
const actualError = error || new Error(errorMessage);
const displayError = this.processError(actualError, context);
// Update statistics
this.updateStats(actualError, context);
// Call custom handler
if (this.config.onError) {
this.config.onError(actualError, displayError, context);
}
// Show toast if enabled
if (this.config.showToasts) {
this.showErrorToast(displayError);
}
// Log to console if enabled
if (this.config.logToConsole) {
console.error('Global Error:', actualError, { context, displayError });
}
}
/**
* Handle unhandled promise rejections
*/
private handleUnhandledRejection(event: PromiseRejectionEvent): void {
// Check rate limiting
if (!this.checkRateLimit()) {
return;
}
// Check if error should be ignored
const reason = event.reason;
const errorMessage = reason instanceof Error ? reason.message : String(reason);
if (this.shouldIgnoreError(errorMessage)) {
return;
}
// Create context
const context: ErrorContext = {
component: 'global-error-handler',
action: 'unhandled-rejection',
metadata: {
promiseString: event.promise.toString(),
reasonType: typeof reason,
userAgent: navigator.userAgent,
url: window.location.href
}
};
// Convert reason to error
const error = reason instanceof Error ? reason : new Error(String(reason));
const displayError = this.processError(error, context);
// Update statistics
this.updateStats(error, context);
// Call custom handler
if (this.config.onUnhandledRejection) {
this.config.onUnhandledRejection(reason, event.promise, displayError);
}
// Call general error handler
if (this.config.onError) {
this.config.onError(error, displayError, context);
}
// Show toast if enabled
if (this.config.showToasts) {
this.showErrorToast(displayError);
}
// Log to console if enabled
if (this.config.logToConsole) {
console.error('Unhandled Promise Rejection:', reason, { context, displayError });
}
// Prevent default browser behavior
event.preventDefault();
}
/**
* Process error through error handler
*/
private processError(error: Error, context: ErrorContext): DisplayError {
const apiError = parseUnknownError(error);
return this.errorHandler.handleApiError(apiError, context);
}
/**
* Check if error should be ignored
*/
private shouldIgnoreError(message: string): boolean {
return this.config.ignoredErrors.some(ignored =>
message.includes(ignored) || message === ignored
);
}
/**
* Check rate limiting
*/
private checkRateLimit(): boolean {
const now = Date.now();
const oneMinuteAgo = now - 60000;
// Remove old error counts
this.errorCounts = this.errorCounts.filter(timestamp => timestamp > oneMinuteAgo);
// Check if we've exceeded the limit
if (this.errorCounts.length >= this.config.maxErrorsPerMinute) {
return false;
}
// Add current error
this.errorCounts.push(now);
return true;
}
/**
* Update error statistics
*/
private updateStats(error: Error, context: ErrorContext): void {
this.stats.totalErrors++;
const errorType = error.constructor.name;
this.stats.errorsByType[errorType] = (this.stats.errorsByType[errorType] || 0) + 1;
this.stats.lastError = {
message: error.message,
timestamp: new Date(),
context
};
// Track errors per minute
const minute = Math.floor(Date.now() / 60000);
if (this.stats.errorsByMinute.length === 0 || this.stats.errorsByMinute[this.stats.errorsByMinute.length - 1] !== minute) {
this.stats.errorsByMinute.push(minute);
}
}
/**
* Clean up old error counts
*/
private cleanupErrorCounts(): void {
const oneMinuteAgo = Date.now() - 60000;
this.errorCounts = this.errorCounts.filter(timestamp => timestamp > oneMinuteAgo);
}
/**
* Show error toast
*/
private showErrorToast(displayError: DisplayError): void {
// Integration point for toast notifications
// Apps should configure this via onError callback
}
/**
* Get error statistics
*/
getStats(): ErrorStats {
return { ...this.stats };
}
/**
* Reset error statistics
*/
resetStats(): void {
this.stats = {
totalErrors: 0,
errorsByType: {},
errorsByMinute: []
};
this.errorCounts = [];
}
/**
* Update configuration
*/
configure(config: Partial<GlobalErrorHandlerConfig>): void {
this.config = { ...this.config, ...config };
if (config.enabled !== undefined) {
if (config.enabled) {
this.install();
} else {
this.uninstall();
}
}
}
/**
* Add error to ignore list
*/
addIgnoredError(pattern: string): void {
if (!this.config.ignoredErrors.includes(pattern)) {
this.config.ignoredErrors.push(pattern);
}
}
/**
* Remove error from ignore list
*/
removeIgnoredError(pattern: string): void {
const index = this.config.ignoredErrors.indexOf(pattern);
if (index > -1) {
this.config.ignoredErrors.splice(index, 1);
}
}
}
/**
* Global error handler instance
*/
let globalErrorHandler: GlobalErrorHandler | null = null;
/**
* Get or create global error handler
*/
export function getGlobalErrorHandler(): GlobalErrorHandler {
if (!globalErrorHandler) {
globalErrorHandler = new GlobalErrorHandler();
}
return globalErrorHandler;
}
/**
* Configure global error handling
*/
export function configureGlobalErrorHandling(config: GlobalErrorHandlerConfig): GlobalErrorHandler {
globalErrorHandler = new GlobalErrorHandler(config);
return globalErrorHandler;
}
/**
* Install global error handling with default configuration
*/
export function installGlobalErrorHandling(config?: GlobalErrorHandlerConfig): GlobalErrorHandler {
return configureGlobalErrorHandling({ enabled: true, ...config });
}