Back to patterns

Stack Trace Analyzer

RUNTIME

A structured approach to runtime error debugging, helping developers navigate complex stack traces, identify error sources, and implement error boundaries.

Diagram for Stack Trace Analyzer

Use Cases

  • Error tracking
  • Stack trace analysis
  • Error boundaries
  • Runtime debugging

Production Implementation

Implementation
// Real-world example: Error tracking and analysis system
interface StackFrame {
  fileName: string;
  lineNumber: number;
  columnNumber: number;
  functionName: string;
  source?: string;
}

interface ErrorReport {
  id: string;
  timestamp: Date;
  error: {
    name: string;
    message: string;
    stack: StackFrame[];
  };
  context: {
    url: string;
    userAgent: string;
    [key: string]: any;
  };
}

class ErrorTracker {
  private static instance: ErrorTracker;
  private reports: ErrorReport[] = [];
  private contextProviders: Map<string, () => any> = new Map();
  private errorListeners: Set<(report: ErrorReport) => void> = new Set();
  private logger: Console;

  private constructor() {
    this.logger = console;
    this.setupGlobalHandlers();
  }

  static getInstance(): ErrorTracker {
    if (!ErrorTracker.instance) {
      ErrorTracker.instance = new ErrorTracker();
    }
    return ErrorTracker.instance;
  }

  private setupGlobalHandlers(): void {
    // Handle uncaught errors
    window.addEventListener('error', (event) => {
      this.handleError(event.error);
    });

    // Handle unhandled promise rejections
    window.addEventListener('unhandledrejection', (event) => {
      this.handleError(event.reason);
    });

    // Override console.error
    const originalError = console.error;
    console.error = (...args) => {
      const error = args[0];
      if (error instanceof Error) {
        this.handleError(error);
      }
      originalError.apply(console, args);
    };
  }

  addContextProvider(name: string, provider: () => any): void {
    this.contextProviders.set(name, provider);
  }

  addEventListener(listener: (report: ErrorReport) => void): () => void {
    this.errorListeners.add(listener);
    return () => this.errorListeners.delete(listener);
  }

  async handleError(error: Error): Promise<void> {
    try {
      const stackFrames = this.parseStackTrace(error);
      const context = this.gatherContext();

      const report: ErrorReport = {
        id: this.generateErrorId(),
        timestamp: new Date(),
        error: {
          name: error.name,
          message: error.message,
          stack: stackFrames
        },
        context
      };

      this.reports.push(report);
      this.notifyListeners(report);
      await this.sendToServer(report);

      this.logger.debug('Error report generated:', report);

    } catch (e) {
      this.logger.error('Failed to handle error:', e);
    }
  }

  private parseStackTrace(error: Error): StackFrame[] {
    const stackLines = error.stack?.split('\n') || [];
    const frames: StackFrame[] = [];

    for (const line of stackLines) {
      const frame = this.parseStackFrame(line);
      if (frame) frames.push(frame);
    }

    return frames;
  }

  private parseStackFrame(line: string): StackFrame | null {
    // Chrome-style stack frame
    const chromeMatch = line.match(
      /at (?:(.+?)s+()?(?:(.+?):(d+):(d+)))?/
    );
    if (chromeMatch) {
      const [, fnName, fileName, lineNo, colNo] = chromeMatch;
      return {
        functionName: fnName || '<anonymous>',
        fileName,
        lineNumber: parseInt(lineNo, 10),
        columnNumber: parseInt(colNo, 10),
        source: line.trim()
      };
    }

    // Firefox-style stack frame
    const firefoxMatch = line.match(
      /(.*)@(.+):(d+):(d+)/
    );
    if (firefoxMatch) {
      const [, fnName, fileName, lineNo, colNo] = firefoxMatch;
      return {
        functionName: fnName || '<anonymous>',
        fileName,
        lineNumber: parseInt(lineNo, 10),
        columnNumber: parseInt(colNo, 10),
        source: line.trim()
      };
    }

    return null;
  }

  private gatherContext(): any {
    const context: any = {
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: new Date().toISOString(),
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight
      }
    };

    // Add custom context from providers
    for (const [name, provider] of this.contextProviders) {
      try {
        context[name] = provider();
      } catch (e) {
        this.logger.warn(`Context provider "${name}" failed:`, e);
      }
    }

    return context;
  }

  private generateErrorId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  private notifyListeners(report: ErrorReport): void {
    for (const listener of this.errorListeners) {
      try {
        listener(report);
      } catch (e) {
        this.logger.error('Error listener failed:', e);
      }
    }
  }

  private async sendToServer(report: ErrorReport): Promise<void> {
    try {
      const response = await fetch('/api/errors', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(report)
      });

      if (!response.ok) {
        throw new Error(`Server responded with ${response.status}`);
      }
    } catch (e) {
      this.logger.error('Failed to send error report:', e);
    }
  }

  getErrorSummary(): {
    total: number;
    recent: ErrorReport[];
    topErrors: Array<{
      name: string;
      count: number;
    }>;
  } {
    const recent = this.reports
      .slice(-10)
      .reverse();

    const errorCounts = this.reports.reduce((acc, report) => {
      const name = report.error.name;
      acc[name] = (acc[name] || 0) + 1;
      return acc;
    }, {} as Record<string, number>);

    const topErrors = Object.entries(errorCounts)
      .map(([name, count]) => ({ name, count }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 5);

    return {
      total: this.reports.length,
      recent,
      topErrors
    };
  }
}

// React Error Boundary implementation
import React from 'react';

interface ErrorBoundaryProps {
  fallback: React.ReactNode;
  onError?: (error: Error, info: React.ErrorInfo) => void;
  children: React.ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends React.Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo): void {
    const { onError } = this.props;
    
    // Track error
    ErrorTracker.getInstance().handleError(error);
    
    // Call custom error handler
    if (onError) {
      onError(error, info);
    }
  }

  render(): React.ReactNode {
    if (this.state.hasError) {
      return this.props.fallback;
    }

    return this.props.children;
  }
}

// Example usage
// Initialize error tracker
const errorTracker = ErrorTracker.getInstance();

// Add context providers
errorTracker.addContextProvider('user', () => ({
  id: getCurrentUser()?.id,
  role: getCurrentUser()?.role
}));

errorTracker.addContextProvider('app', () => ({
  version: process.env.VERSION,
  environment: process.env.NODE_ENV
}));

// Add error listener
errorTracker.addEventListener((report) => {
  // Send to monitoring service
  monitoring.trackError(report);
});

// Use in React application
function App() {
  return (
    <ErrorBoundary
      fallback={<ErrorPage />}
      onError={(error, info) => {
        // Custom error handling
        notifySupport(error);
      }}
    >
      <MainContent />
    </ErrorBoundary>
  );
}

// Example error handling in async function
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    errorTracker.handleError(error);
    throw error;
  }
}

Code Examples

Common Error Handling Issues

typescript
// ❌ Common error handling issues
class Service {
  // Issue 1: Generic error handling
  async fetchData() {
    try {
      const response = await fetch('/api/data');
      return await response.json();
    } catch (error) {
      console.error('Error:', error);
    }
  }
  
  // Issue 2: Swallowing errors
  processItem(item: any) {
    try {
      return this.transform(item);
    } catch {
      return null;
    }
  }
  
  // Issue 3: No error boundaries
  render() {
    return <Component />;
  }
  
  // Issue 4: Poor error information
  async saveData(data: any) {
    if (!this.validate(data)) {
      throw new Error('Invalid data');
    }
  }
}

Common issues include generic error handling, swallowing errors, missing error boundaries, and poor error information.

Robust Error Handling

typescript
// ✅ Robust error handling
class Service {
  // Specific error types
  class APIError extends Error {
    constructor(
      message: string,
      public status: number,
      public code: string
    ) {
      super(message);
      this.name = 'APIError';
    }
  }

  // Proper error handling with context
  async fetchData() {
    try {
      const response = await fetch('/api/data');
      
      if (!response.ok) {
        throw new APIError(
          'API request failed',
          response.status,
          await response.text()
        );
      }
      
      return await response.json();
      
    } catch (error) {
      // Track error with context
      ErrorTracker.getInstance().handleError(error, {
        endpoint: '/api/data',
        method: 'GET'
      });
      
      // Rethrow for boundary handling
      throw error;
    }
  }

  // Error boundary implementation
  render() {
    return (
      <ErrorBoundary
        fallback={<ErrorDisplay />}
        onError={(error) => {
          // Log error
          logger.error('Component error:', error);
          
          // Show user feedback
          notifications.show({
            type: 'error',
            message: 'Something went wrong'
          });
        }}
      >
        <Component />
      </ErrorBoundary>
    );
  }

  // Detailed error information
  async saveData(data: any) {
    const validation = this.validate(data);
    
    if (!validation.valid) {
      throw new ValidationError(
        'Data validation failed',
        validation.errors,
        {
          fields: validation.failedFields,
          data: JSON.stringify(data)
        }
      );
    }
  }

  // Cleanup on error
  async processItems(items: any[]) {
    const processed = new Set();
    
    try {
      for (const item of items) {
        await this.process(item);
        processed.add(item.id);
      }
      
    } catch (error) {
      // Cleanup processed items
      await this.rollback(Array.from(processed));
      throw error;
    }
  }
}

This implementation includes specific error types, proper error tracking, error boundaries, and cleanup handling.

Best Practices

  • Use specific error types
  • Include error context
  • Implement error boundaries
  • Add error tracking
  • Handle async errors
  • Provide cleanup mechanisms
  • Log error details

Common Pitfalls

  • Generic error handling
  • Swallowing errors
  • Missing error boundaries
  • Poor error information
  • No error tracking
  • Inconsistent error handling
  • Missing cleanup on error