Skip to content

Error Handling

HTML Layout Parser provides comprehensive error handling mechanisms to help you diagnose and handle various parsing issues.

Error Types

Error Code Classification

Code RangeCategoryDescription
0SuccessOperation completed successfully
1xxxInput ValidationHTML, CSS, or parameter validation failures
2xxxFont ErrorsFont loading, unloading, or usage errors
3xxxParse ErrorsErrors during HTML/CSS parsing
4xxxMemory ErrorsMemory allocation or management errors
5xxxInternal ErrorsSystem internal errors

Common Error Codes

typescript
// Input Validation Errors (1xxx)
const INPUT_ERRORS = {
  1001: 'HTML content is empty',
  1002: 'Invalid viewport width',
  1003: 'CSS syntax error',
  1004: 'Parameter type error',
  1005: 'Document size exceeds limit'
};

// Font Errors (2xxx)
const FONT_ERRORS = {
  2001: 'Invalid font data',
  2002: 'Font loading failed',
  2003: 'Font ID does not exist',
  2004: 'Font format not supported',
  2005: 'Default font not set'
};

// Parse Errors (3xxx)
const PARSE_ERRORS = {
  3001: 'HTML parsing failed',
  3002: 'CSS parsing failed',
  3003: 'Layout calculation failed',
  3004: 'Parse timeout',
  3005: 'Unsupported CSS property'
};

// Memory Errors (4xxx)
const MEMORY_ERRORS = {
  4001: 'Memory allocation failed',
  4002: 'Memory usage exceeds limit',
  4003: 'Buffer overflow',
  4004: 'Memory leak detected'
};

// Internal Errors (5xxx)
const INTERNAL_ERRORS = {
  5001: 'WASM module not initialized',
  5002: 'Internal state error',
  5003: 'Insufficient system resources',
  5004: 'Unknown internal error'
};

Basic Error Handling

Using try-catch

typescript
async function basicErrorHandling() {
  const parser = new HtmlLayoutParser();
  
  try {
    await parser.init();
    
    const fontData = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
    const fontId = parser.loadFont(new Uint8Array(fontData), 'Arial');
    
    if (fontId <= 0) {
      throw new Error('Font loading failed');
    }
    
    parser.setDefaultFont(fontId);
    
    const layouts = parser.parse(html, { viewportWidth: 800 });
    return layouts;
    
  } catch (error) {
    console.error('Error during parsing:', error);
    
    // Handle based on error type
    if (error.message.includes('font')) {
      console.error('Font-related error, please check font file');
    } else if (error.message.includes('HTML')) {
      console.error('HTML parsing error, please check HTML format');
    } else {
      console.error('Unknown error:', error);
    }
    
    return null;
  } finally {
    parser.destroy();
  }
}

Using Diagnostic Mode

typescript
function diagnosticErrorHandling(html: string) {
  const parser = new HtmlLayoutParser();
  
  try {
    // Use diagnostic mode for detailed error information
    const result = parser.parseWithDiagnostics(html, {
      viewportWidth: 800,
      enableMetrics: true
    });
    
    if (result.success) {
      console.log('Parse successful');
      
      // Check warnings
      if (result.warnings && result.warnings.length > 0) {
        console.warn('Parse warnings:');
        result.warnings.forEach(warning => {
          console.warn(`  - ${warning.message}`);
          if (warning.line) {
            console.warn(`    Location: line ${warning.line}, column ${warning.column}`);
          }
        });
      }
      
      return result.data;
    } else {
      console.error('Parse failed');
      
      // Handle errors
      if (result.errors) {
        result.errors.forEach(error => {
          console.error(`Error [${error.code}]: ${error.message}`);
          
          if (error.line && error.column) {
            console.error(`  Location: line ${error.line}, column ${error.column}`);
          }
          
          if (error.suggestion) {
            console.error(`  Suggestion: ${error.suggestion}`);
          }
        });
      }
      
      return null;
    }
  } finally {
    parser.destroy();
  }
}

Advanced Error Handling

Error Classification Handler

typescript
class ErrorHandler {
  private parser: HtmlLayoutParser;
  private retryCount: Map<string, number> = new Map();
  private maxRetries: number = 3;

  constructor(parser: HtmlLayoutParser, maxRetries: number = 3) {
    this.parser = parser;
    this.maxRetries = maxRetries;
  }

  async handleParseWithRetry(
    html: string, 
    options: { viewportWidth: number },
    retryKey?: string
  ): Promise<CharLayout[] | null> {
    const key = retryKey || this.generateRetryKey(html, options);
    const currentRetries = this.retryCount.get(key) || 0;

    try {
      const result = this.parser.parseWithDiagnostics(html, options);
      
      if (result.success) {
        // Reset retry count on success
        this.retryCount.delete(key);
        return result.data;
      } else {
        return this.handleParseErrors(result.errors || [], html, options, key);
      }
    } catch (error) {
      return this.handleException(error, html, options, key);
    }
  }

  private async handleParseErrors(
    errors: any[],
    html: string,
    options: { viewportWidth: number },
    retryKey: string
  ): Promise<CharLayout[] | null> {
    for (const error of errors) {
      const errorCode = error.code;
      
      switch (Math.floor(errorCode / 1000)) {
        case 1: // Input validation errors
          return this.handleInputError(error, html, options);
          
        case 2: // Font errors
          const fontFixed = await this.handleFontError(error);
          if (fontFixed) {
            return this.retryParse(html, options, retryKey);
          }
          break;
          
        case 3: // Parse errors
          const parseFixed = this.handleParseError(error, html);
          if (parseFixed) {
            return this.retryParse(parseFixed, options, retryKey);
          }
          break;
          
        case 4: // Memory errors
          const memoryFixed = await this.handleMemoryError(error);
          if (memoryFixed) {
            return this.retryParse(html, options, retryKey);
          }
          break;
          
        case 5: // Internal errors
          console.error('Internal error, cannot auto-fix:', error);
          break;
      }
    }
    
    return null;
  }

  private handleInputError(error: any, html: string, options: { viewportWidth: number }): CharLayout[] | null {
    switch (error.code) {
      case 1001: // HTML content is empty
        console.warn('HTML content is empty, returning empty result');
        return [];
        
      case 1002: // Invalid viewport width
        console.warn('Invalid viewport width, using default 800');
        return this.parser.parse(html, { ...options, viewportWidth: 800 });
        
      case 1005: // Document size exceeds limit
        console.warn('Document too large, attempting truncation');
        const truncatedHtml = html.substring(0, 50000);
        return this.parser.parse(truncatedHtml, options);
        
      default:
        console.error('Cannot handle input error:', error);
        return null;
    }
  }

  private async handleFontError(error: any): Promise<boolean> {
    switch (error.code) {
      case 2005: // Default font not set
        console.warn('Default font not set, attempting to load system font');
        try {
          // Try to load a basic font
          const response = await fetch('/fonts/arial.ttf');
          if (response.ok) {
            const fontData = new Uint8Array(await response.arrayBuffer());
            const fontId = this.parser.loadFont(fontData, 'Arial');
            if (fontId > 0) {
              this.parser.setDefaultFont(fontId);
              return true;
            }
          }
        } catch (e) {
          console.error('Failed to load default font:', e);
        }
        return false;
        
      case 2001: // Invalid font data
      case 2002: // Font loading failed
        console.warn('Font issue, clearing and reloading fonts');
        this.parser.clearAllFonts();
        // Can attempt to reload fonts here
        return false;
        
      default:
        return false;
    }
  }

  private handleParseError(error: any, html: string): string | null {
    switch (error.code) {
      case 3004: // Parse timeout
        console.warn('Parse timeout, attempting to simplify HTML');
        // Remove complex CSS
        return html.replace(/style\s*=\s*"[^"]*"/g, '');
        
      case 3005: // Unsupported CSS property
        console.warn('Contains unsupported CSS, attempting cleanup');
        // Remove all inline styles
        return html.replace(/style\s*=\s*"[^"]*"/g, '');
        
      default:
        return null;
    }
  }

  private async handleMemoryError(error: any): Promise<boolean> {
    switch (error.code) {
      case 4002: // Memory usage exceeds limit
        console.warn('High memory usage, clearing cache and fonts');
        this.parser.clearCache();
        
        // Keep only one default font
        const fonts = this.parser.getLoadedFonts();
        if (fonts.length > 1) {
          for (let i = 1; i < fonts.length; i++) {
            this.parser.unloadFont(fonts[i].id);
          }
        }
        return true;
        
      default:
        return false;
    }
  }

  private async retryParse(
    html: string,
    options: { viewportWidth: number },
    retryKey: string
  ): Promise<CharLayout[] | null> {
    const currentRetries = this.retryCount.get(retryKey) || 0;
    
    if (currentRetries >= this.maxRetries) {
      console.error(`Retry limit reached (${this.maxRetries}), giving up`);
      return null;
    }
    
    this.retryCount.set(retryKey, currentRetries + 1);
    console.log(`Retry attempt ${currentRetries + 1}...`);
    
    // Wait a bit before retrying
    await new Promise(resolve => setTimeout(resolve, 100 * (currentRetries + 1)));
    
    return this.handleParseWithRetry(html, options, retryKey);
  }

  private handleException(
    error: any,
    html: string,
    options: { viewportWidth: number },
    retryKey: string
  ): Promise<CharLayout[] | null> {
    console.error('Exception during parsing:', error);
    
    // Check if it's a memory-related exception
    if (error.message.includes('memory') || error.message.includes('allocation')) {
      console.warn('Detected memory-related exception, attempting cleanup');
      this.parser.clearCache();
      this.parser.clearAllFonts();
      
      return this.retryParse(html, options, retryKey);
    }
    
    // Check if it's an initialization-related exception
    if (error.message.includes('not initialized')) {
      console.warn('Parser not initialized, needs external reinitialization');
      // External reinitialization needed
      return Promise.resolve(null);
    }
    
    return Promise.resolve(null);
  }

  private generateRetryKey(html: string, options: { viewportWidth: number }): string {
    // Generate unique key based on content and options
    const contentHash = this.simpleHash(html);
    return `${contentHash}-${options.viewportWidth}`;
  }

  private simpleHash(str: string): string {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // Convert to 32-bit integer
    }
    return hash.toString();
  }

  clearRetryHistory(): void {
    this.retryCount.clear();
  }
}

// Usage example
const parser = new HtmlLayoutParser();
await parser.init();

const errorHandler = new ErrorHandler(parser, 3);

const layouts = await errorHandler.handleParseWithRetry(html, {
  viewportWidth: 800
});

if (layouts) {
  console.log('Parse successful, got', layouts.length, 'character layouts');
} else {
  console.error('Parse ultimately failed');
}

Error Recovery Strategies

typescript
class ErrorRecoveryManager {
  private parser: HtmlLayoutParser;
  private fallbackStrategies: Array<(html: string, options: any) => Promise<CharLayout[] | null>>;

  constructor(parser: HtmlLayoutParser) {
    this.parser = parser;
    this.fallbackStrategies = [
      this.strategySimplifyCSS.bind(this),
      this.strategyRemoveComplexElements.bind(this),
      this.strategyBasicTextOnly.bind(this),
      this.strategyEmergencyFallback.bind(this)
    ];
  }

  async parseWithRecovery(html: string, options: { viewportWidth: number }): Promise<{
    success: boolean;
    data: CharLayout[] | null;
    strategy: string;
    attempts: number;
  }> {
    // First try normal parsing
    try {
      const result = this.parser.parseWithDiagnostics(html, options);
      if (result.success) {
        return {
          success: true,
          data: result.data,
          strategy: 'normal',
          attempts: 1
        };
      }
    } catch (error) {
      console.warn('Normal parse failed, starting error recovery:', error.message);
    }

    // Try recovery strategies in sequence
    for (let i = 0; i < this.fallbackStrategies.length; i++) {
      const strategy = this.fallbackStrategies[i];
      const strategyName = this.getStrategyName(i);
      
      console.log(`Trying recovery strategy: ${strategyName}`);
      
      try {
        const result = await strategy(html, options);
        if (result && result.length > 0) {
          return {
            success: true,
            data: result,
            strategy: strategyName,
            attempts: i + 2
          };
        }
      } catch (error) {
        console.warn(`Recovery strategy ${strategyName} failed:`, error.message);
      }
    }

    return {
      success: false,
      data: null,
      strategy: 'all_failed',
      attempts: this.fallbackStrategies.length + 1
    };
  }

  private async strategySimplifyCSS(html: string, options: any): Promise<CharLayout[] | null> {
    // Strategy 1: Simplify CSS
    const simplifiedHtml = html
      .replace(/style\s*=\s*"[^"]*"/g, '') // Remove inline styles
      .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') // Remove style tags
      .replace(/class\s*=\s*"[^"]*"/g, ''); // Remove class attributes

    return this.parser.parse(simplifiedHtml, options);
  }

  private async strategyRemoveComplexElements(html: string, options: any): Promise<CharLayout[] | null> {
    // Strategy 2: Remove complex elements
    const simplifiedHtml = html
      .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') // Remove script
      .replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, '') // Remove iframe
      .replace(/<object[^>]*>[\s\S]*?<\/object>/gi, '') // Remove object
      .replace(/<embed[^>]*>/gi, '') // Remove embed
      .replace(/<canvas[^>]*>[\s\S]*?<\/canvas>/gi, '') // Remove canvas
      .replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, ''); // Remove svg

    return this.parser.parse(simplifiedHtml, options);
  }

  private async strategyBasicTextOnly(html: string, options: any): Promise<CharLayout[] | null> {
    // Strategy 3: Keep only basic text elements
    const textOnlyHtml = html
      .replace(/<(?!\/?(div|p|span|h[1-6]|br|strong|em|b|i)\b)[^>]*>/gi, '') // Keep only basic text tags
      .replace(/\s+/g, ' ') // Merge whitespace
      .trim();

    if (!textOnlyHtml) {
      return null;
    }

    return this.parser.parse(`<div>${textOnlyHtml}</div>`, options);
  }

  private async strategyEmergencyFallback(html: string, options: any): Promise<CharLayout[] | null> {
    // Strategy 4: Emergency fallback - extract plain text
    const textContent = html
      .replace(/<[^>]*>/g, ' ') // Remove all tags
      .replace(/\s+/g, ' ') // Merge whitespace
      .trim();

    if (!textContent) {
      return [];
    }

    // Create simplest HTML
    const emergencyHtml = `<div>${textContent}</div>`;
    
    try {
      return this.parser.parse(emergencyHtml, {
        ...options,
        maxCharacters: Math.min(textContent.length, 1000) // Limit length
      });
    } catch (error) {
      // Final fallback: return empty array instead of failure
      console.warn('All recovery strategies failed, returning empty result');
      return [];
    }
  }

  private getStrategyName(index: number): string {
    const names = [
      'simplify_css',
      'remove_complex_elements',
      'basic_text_only',
      'emergency_fallback'
    ];
    return names[index] || 'unknown';
  }
}

// Usage example
const parser = new HtmlLayoutParser();
await parser.init();

const recoveryManager = new ErrorRecoveryManager(parser);

const result = await recoveryManager.parseWithRecovery(html, {
  viewportWidth: 800
});

if (result.success) {
  console.log(`Parse successful (strategy: ${result.strategy}, attempts: ${result.attempts})`);
  console.log('Character count:', result.data?.length);
} else {
  console.error('All recovery strategies failed');
}

Error Monitoring and Reporting

Error Statistics Collector

typescript
class ErrorStatistics {
  private stats: Map<number, {
    count: number;
    lastOccurrence: number;
    samples: string[];
  }> = new Map();

  recordError(errorCode: number, context?: string): void {
    const existing = this.stats.get(errorCode) || {
      count: 0,
      lastOccurrence: 0,
      samples: []
    };

    existing.count++;
    existing.lastOccurrence = Date.now();
    
    if (context && existing.samples.length < 5) {
      existing.samples.push(context);
    }

    this.stats.set(errorCode, existing);
  }

  getErrorReport(): {
    totalErrors: number;
    errorsByCategory: Record<string, number>;
    topErrors: Array<{ code: number; count: number; category: string }>;
    recentErrors: Array<{ code: number; timestamp: number }>;
  } {
    const totalErrors = Array.from(this.stats.values())
      .reduce((sum, stat) => sum + stat.count, 0);

    const errorsByCategory: Record<string, number> = {};
    const topErrors: Array<{ code: number; count: number; category: string }> = [];
    const recentErrors: Array<{ code: number; timestamp: number }> = [];

    for (const [code, stat] of this.stats) {
      const category = this.getCategoryName(code);
      errorsByCategory[category] = (errorsByCategory[category] || 0) + stat.count;
      
      topErrors.push({ code, count: stat.count, category });
      
      if (Date.now() - stat.lastOccurrence < 3600000) { // Last 1 hour
        recentErrors.push({ code, timestamp: stat.lastOccurrence });
      }
    }

    topErrors.sort((a, b) => b.count - a.count);
    recentErrors.sort((a, b) => b.timestamp - a.timestamp);

    return {
      totalErrors,
      errorsByCategory,
      topErrors: topErrors.slice(0, 10),
      recentErrors: recentErrors.slice(0, 20)
    };
  }

  private getCategoryName(errorCode: number): string {
    const category = Math.floor(errorCode / 1000);
    const categories: Record<number, string> = {
      1: 'Input Validation',
      2: 'Font Related',
      3: 'Parse Errors',
      4: 'Memory Errors',
      5: 'Internal Errors'
    };
    return categories[category] || 'Unknown';
  }

  clear(): void {
    this.stats.clear();
  }

  exportStats(): string {
    return JSON.stringify(Object.fromEntries(this.stats), null, 2);
  }
}

// Usage example
const errorStats = new ErrorStatistics();

// Record in error handling
try {
  const layouts = parser.parse(html, { viewportWidth: 800 });
} catch (error) {
  // Assuming we can extract error code from error
  const errorCode = extractErrorCode(error);
  errorStats.recordError(errorCode, html.substring(0, 100));
}

// Generate report periodically
setInterval(() => {
  const report = errorStats.getErrorReport();
  console.log('Error statistics report:', report);
}, 300000); // Every 5 minutes

Error Reporting System

typescript
class ErrorReporter {
  private endpoint: string;
  private apiKey: string;
  private batchSize: number;
  private errorQueue: Array<{
    timestamp: number;
    errorCode: number;
    message: string;
    context: any;
    userAgent: string;
    sessionId: string;
  }> = [];

  constructor(endpoint: string, apiKey: string, batchSize: number = 10) {
    this.endpoint = endpoint;
    this.apiKey = apiKey;
    this.batchSize = batchSize;
  }

  reportError(
    errorCode: number,
    message: string,
    context: any = {},
    sessionId: string = 'unknown'
  ): void {
    this.errorQueue.push({
      timestamp: Date.now(),
      errorCode,
      message,
      context,
      userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'Node.js',
      sessionId
    });

    if (this.errorQueue.length >= this.batchSize) {
      this.flushErrors();
    }
  }

  private async flushErrors(): Promise<void> {
    if (this.errorQueue.length === 0) return;

    const errors = [...this.errorQueue];
    this.errorQueue = [];

    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${this.apiKey}`
        },
        body: JSON.stringify({
          errors,
          timestamp: Date.now(),
          version: '0.0.1'
        })
      });
    } catch (error) {
      console.warn('Error reporting failed:', error);
      // Re-add errors to queue
      this.errorQueue.unshift(...errors);
    }
  }

  async flush(): Promise<void> {
    await this.flushErrors();
  }
}

// Usage example
const errorReporter = new ErrorReporter(
  'https://api.example.com/errors',
  'your-api-key',
  5
);

// Use in error handling
try {
  const layouts = parser.parse(html, { viewportWidth: 800 });
} catch (error) {
  const errorCode = extractErrorCode(error);
  
  errorReporter.reportError(
    errorCode,
    error.message,
    {
      htmlLength: html.length,
      viewportWidth: 800,
      stackTrace: error.stack
    },
    generateSessionId()
  );
  
  // Continue error handling...
}

// Ensure all errors are reported before app closes
process.on('beforeExit', async () => {
  await errorReporter.flush();
});

Production Environment Error Handling

Graceful Degradation

typescript
class GracefulDegradation {
  private parser: HtmlLayoutParser;
  private fallbackRenderer: (html: string) => CharLayout[];

  constructor(parser: HtmlLayoutParser) {
    this.parser = parser;
    this.fallbackRenderer = this.createFallbackRenderer();
  }

  async parseWithGracefulDegradation(
    html: string,
    options: { viewportWidth: number }
  ): Promise<{
    layouts: CharLayout[];
    degraded: boolean;
    reason?: string;
  }> {
    try {
      // Try normal parsing
      const layouts = this.parser.parse(html, options);
      return {
        layouts,
        degraded: false
      };
    } catch (error) {
      console.warn('Normal parse failed, enabling graceful degradation:', error.message);
      
      try {
        // Try simplified parsing
        const simplifiedLayouts = await this.trySimplifiedParse(html, options);
        if (simplifiedLayouts) {
          return {
            layouts: simplifiedLayouts,
            degraded: true,
            reason: 'simplified_parse'
          };
        }
      } catch (simplifiedError) {
        console.warn('Simplified parse also failed:', simplifiedError.message);
      }

      // Final fallback: use plain text rendering
      const fallbackLayouts = this.fallbackRenderer(html);
      return {
        layouts: fallbackLayouts,
        degraded: true,
        reason: 'fallback_renderer'
      };
    }
  }

  private async trySimplifiedParse(
    html: string,
    options: { viewportWidth: number }
  ): Promise<CharLayout[] | null> {
    // Remove complex CSS and elements
    const simplifiedHtml = html
      .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
      .replace(/style\s*=\s*"[^"]*"/g, '')
      .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
      .replace(/<(?!\/?(div|p|span|h[1-6]|br|strong|em|b|i|a)\b)[^>]*>/gi, '');

    return this.parser.parse(simplifiedHtml, {
      ...options,
      maxCharacters: 10000,
      timeout: 5000
    });
  }

  private createFallbackRenderer(): (html: string) => CharLayout[] {
    return (html: string) => {
      // Extract plain text
      const text = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
      
      // Create basic character layouts
      const layouts: CharLayout[] = [];
      let x = 0;
      const y = 20; // Baseline position
      const fontSize = 16;
      const charWidth = fontSize * 0.6; // Estimated character width

      for (let i = 0; i < text.length; i++) {
        const char = text[i];
        
        if (char === ' ') {
          x += charWidth;
          continue;
        }

        layouts.push({
          character: char,
          x,
          y,
          width: charWidth,
          height: fontSize,
          baseline: y,
          fontFamily: 'Arial',
          fontSize,
          fontWeight: 400,
          fontStyle: 'normal',
          fontId: 0,
          color: '#000000FF',
          backgroundColor: '#00000000',
          opacity: 1,
          textDecoration: {
            underline: false,
            overline: false,
            lineThrough: false,
            color: '#000000FF',
            style: 'solid',
            thickness: 1
          },
          letterSpacing: 0,
          wordSpacing: 0,
          transform: {
            scaleX: 1,
            scaleY: 1,
            skewX: 0,
            skewY: 0,
            rotate: 0
          },
          direction: 'ltr'
        });

        x += charWidth;
      }

      return layouts;
    };
  }
}

// Usage example
const gracefulDegradation = new GracefulDegradation(parser);

const result = await gracefulDegradation.parseWithGracefulDegradation(html, {
  viewportWidth: 800
});

if (result.degraded) {
  console.warn(`Using degraded mode: ${result.reason}`);
  // May need to notify user or log
}

// Use result
renderToCanvas(ctx, result.layouts);

Error Boundary Component

typescript
class ParserErrorBoundary {
  private parser: HtmlLayoutParser;
  private errorCallback?: (error: Error, html: string) => void;
  private maxConsecutiveErrors: number;
  private consecutiveErrors: number = 0;
  private lastErrorTime: number = 0;

  constructor(
    parser: HtmlLayoutParser,
    options: {
      maxConsecutiveErrors?: number;
      errorCallback?: (error: Error, html: string) => void;
    } = {}
  ) {
    this.parser = parser;
    this.maxConsecutiveErrors = options.maxConsecutiveErrors || 5;
    this.errorCallback = options.errorCallback;
  }

  async safeParse(
    html: string,
    options: { viewportWidth: number }
  ): Promise<CharLayout[] | null> {
    // Check if parsing should be paused (too many consecutive errors)
    if (this.shouldPause()) {
      console.warn('Too many consecutive errors, pausing parsing');
      return null;
    }

    try {
      const layouts = this.parser.parse(html, options);
      
      // Reset error count on success
      this.consecutiveErrors = 0;
      return layouts;
      
    } catch (error) {
      this.handleError(error, html);
      return null;
    }
  }

  private shouldPause(): boolean {
    const now = Date.now();
    const timeSinceLastError = now - this.lastErrorTime;
    
    // Reset count if more than 1 minute since last error
    if (timeSinceLastError > 60000) {
      this.consecutiveErrors = 0;
      return false;
    }

    return this.consecutiveErrors >= this.maxConsecutiveErrors;
  }

  private handleError(error: Error, html: string): void {
    this.consecutiveErrors++;
    this.lastErrorTime = Date.now();

    console.error(`Parse error (consecutive #${this.consecutiveErrors}):`, error.message);

    if (this.errorCallback) {
      try {
        this.errorCallback(error, html);
      } catch (callbackError) {
        console.error('Error callback execution failed:', callbackError);
      }
    }

    // If too many consecutive errors, suggest parser restart
    if (this.consecutiveErrors >= this.maxConsecutiveErrors) {
      console.error('Too many consecutive errors, recommend restarting parser');
    }
  }

  reset(): void {
    this.consecutiveErrors = 0;
    this.lastErrorTime = 0;
  }

  getErrorCount(): number {
    return this.consecutiveErrors;
  }
}

// Usage example
const errorBoundary = new ParserErrorBoundary(parser, {
  maxConsecutiveErrors: 3,
  errorCallback: (error, html) => {
    // Log error to monitoring system
    console.error('Parse error:', {
      message: error.message,
      htmlLength: html.length,
      timestamp: new Date().toISOString()
    });
  }
});

// Use in application
const layouts = await errorBoundary.safeParse(html, { viewportWidth: 800 });

if (layouts) {
  // Normal processing
  renderToCanvas(ctx, layouts);
} else {
  // Show error state or use fallback
  showErrorMessage('Parse failed, please try again later');
}

Released under the MIT License.