CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-single-spa

The router for easy microfrontends that enables multiple frameworks to coexist on the same page.

Pending
Overview
Eval results
Files

error-handling.mddocs/

Error Handling

Global error handling system for managing application errors and failures in microfrontend architectures.

Capabilities

Add Error Handler

Registers a global error handler that will be called when applications or parcels encounter errors during their lifecycle.

/**
 * Add a global error handler for application and parcel errors
 * @param handler - Function to handle errors
 */
function addErrorHandler(handler: (error: AppError) => void): void;

interface AppError extends Error {
  appOrParcelName: string;
}

Usage Examples:

import { addErrorHandler } from "single-spa";

// Basic error logging
addErrorHandler((error) => {
  console.error("Single-SPA Error:", error);
  console.error("Failed app/parcel:", error.appOrParcelName);
  console.error("Error message:", error.message);
  console.error("Stack trace:", error.stack);
});

// Error reporting to external service
addErrorHandler((error) => {
  // Send to error tracking service
  errorTrackingService.captureException(error, {
    tags: {
      component: error.appOrParcelName,
      framework: "single-spa"
    },
    extra: {
      userAgent: navigator.userAgent,
      url: window.location.href,
      timestamp: new Date().toISOString()
    }
  });
});

// Recovery strategies
addErrorHandler((error) => {
  const appName = error.appOrParcelName;
  
  // Log error
  console.error(`Application ${appName} failed:`, error);
  
  // Attempt recovery based on error type
  if (error.message.includes("network")) {
    // Network error - might be temporary
    console.log(`Scheduling retry for ${appName} due to network error`);
    scheduleRetry(appName, 5000);
  } else if (error.message.includes("timeout")) {
    // Timeout error - increase timeout and retry
    console.log(`Increasing timeout for ${appName}`);
    increaseTimeoutAndRetry(appName);
  } else {
    // Other errors - mark app as broken
    console.log(`Marking ${appName} as broken`);
    markApplicationAsBroken(appName);
  }
});

Remove Error Handler

Removes a previously registered error handler.

/**
 * Remove a previously registered error handler
 * @param handler - The error handler function to remove
 */
function removeErrorHandler(handler: (error: AppError) => void): void;

Usage Examples:

import { addErrorHandler, removeErrorHandler } from "single-spa";

// Store handler reference for later removal
const myErrorHandler = (error) => {
  console.log("Handling error:", error);
};

// Add handler
addErrorHandler(myErrorHandler);

// Later, remove the handler
removeErrorHandler(myErrorHandler);

// Conditional error handling
class ConditionalErrorHandler {
  constructor() {
    this.handler = this.handleError.bind(this);
    this.enabled = true;
  }
  
  enable() {
    if (!this.enabled) {
      addErrorHandler(this.handler);
      this.enabled = true;
    }
  }
  
  disable() {
    if (this.enabled) {
      removeErrorHandler(this.handler);
      this.enabled = false;
    }
  }
  
  handleError(error) {
    if (this.enabled) {
      console.error("Conditional handler:", error);
    }
  }
}

Advanced Error Handling Patterns

Error Classification and Recovery

import { addErrorHandler, getAppStatus, triggerAppChange } from "single-spa";

class ErrorManager {
  constructor() {
    this.errorCounts = new Map();
    this.maxRetries = 3;
    this.retryDelay = 1000;
    
    addErrorHandler(this.handleError.bind(this));
  }
  
  handleError(error) {
    const appName = error.appOrParcelName;
    const errorType = this.classifyError(error);
    
    console.error(`${errorType} error in ${appName}:`, error);
    
    switch (errorType) {
      case "LOAD_ERROR":
        this.handleLoadError(appName, error);
        break;
      case "LIFECYCLE_ERROR":
        this.handleLifecycleError(appName, error);
        break;
      case "TIMEOUT_ERROR":
        this.handleTimeoutError(appName, error);
        break;
      case "NETWORK_ERROR":
        this.handleNetworkError(appName, error);
        break;
      default:
        this.handleGenericError(appName, error);
    }
  }
  
  classifyError(error) {
    const message = error.message.toLowerCase();
    
    if (message.includes("loading") || message.includes("import")) {
      return "LOAD_ERROR";
    }
    if (message.includes("timeout")) {
      return "TIMEOUT_ERROR";
    }
    if (message.includes("network") || message.includes("fetch")) {
      return "NETWORK_ERROR";
    }
    if (message.includes("mount") || message.includes("unmount") || message.includes("bootstrap")) {
      return "LIFECYCLE_ERROR";
    }
    
    return "GENERIC_ERROR";
  }
  
  async handleLoadError(appName, error) {
    const count = this.getErrorCount(appName);
    
    if (count < this.maxRetries) {
      console.log(`Retrying load for ${appName} (attempt ${count + 1})`);
      await this.delay(this.retryDelay * Math.pow(2, count)); // Exponential backoff
      triggerAppChange();
    } else {
      console.error(`Max retries exceeded for ${appName}, marking as broken`);
      this.notifyUser(`Application ${appName} is currently unavailable`);
    }
  }
  
  async handleNetworkError(appName, error) {
    console.log(`Network error for ${appName}, will retry when network is available`);
    
    // Wait for network to be available
    await this.waitForNetwork();
    triggerAppChange();
  }
  
  handleTimeoutError(appName, error) {
    console.log(`Timeout error for ${appName}, consider increasing timeout`);
    this.notifyDevelopers(`Timeout issue detected in ${appName}`);
  }
  
  getErrorCount(appName) {
    const count = this.errorCounts.get(appName) || 0;
    this.errorCounts.set(appName, count + 1);
    return count;
  }
  
  async delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  
  async waitForNetwork() {
    return new Promise(resolve => {
      const checkNetwork = () => {
        if (navigator.onLine) {
          resolve();
        } else {
          setTimeout(checkNetwork, 1000);
        }
      };
      checkNetwork();
    });
  }
  
  notifyUser(message) {
    // Show user-friendly error message
    console.log("User notification:", message);
  }
  
  notifyDevelopers(message) {
    // Send to development team
    console.log("Developer notification:", message);
  }
}

Error Boundary for Applications

import { addErrorHandler, getAppStatus, unloadApplication } from "single-spa";

class ApplicationErrorBoundary {
  constructor() {
    this.failedApps = new Set();
    this.recoveryAttempts = new Map();
    this.maxRecoveryAttempts = 2;
    
    addErrorHandler(this.boundError.bind(this));
  }
  
  boundError(error) {
    const appName = error.appOrParcelName;
    
    if (this.failedApps.has(appName)) {
      // App already failed, don't retry
      return;
    }
    
    console.error(`Error boundary caught error in ${appName}:`, error);
    
    // Mark app as failed
    this.failedApps.add(appName);
    
    // Attempt recovery
    this.attemptRecovery(appName);
  }
  
  async attemptRecovery(appName) {
    const attempts = this.recoveryAttempts.get(appName) || 0;
    
    if (attempts >= this.maxRecoveryAttempts) {
      console.error(`Recovery failed for ${appName} after ${attempts} attempts`);
      this.handleUnrecoverableError(appName);
      return;
    }
    
    console.log(`Attempting recovery for ${appName} (attempt ${attempts + 1})`);
    this.recoveryAttempts.set(appName, attempts + 1);
    
    try {
      // Unload and reload the application
      await unloadApplication(appName, { waitForUnmount: true });
      
      // Wait a bit before triggering reload
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      // Trigger app change to reload
      await triggerAppChange();
      
      // Check if recovery was successful
      setTimeout(() => {
        const status = getAppStatus(appName);
        if (status === "MOUNTED") {
          console.log(`Recovery successful for ${appName}`);
          this.failedApps.delete(appName);
          this.recoveryAttempts.delete(appName);
        } else {
          console.log(`Recovery failed for ${appName}, status: ${status}`);
          this.attemptRecovery(appName);
        }
      }, 3000);
      
    } catch (recoveryError) {
      console.error(`Recovery attempt failed for ${appName}:`, recoveryError);
      this.attemptRecovery(appName);
    }
  }
  
  handleUnrecoverableError(appName) {
    console.error(`Application ${appName} is unrecoverable`);
    
    // Remove from failed apps to prevent further attempts
    this.failedApps.delete(appName);
    this.recoveryAttempts.delete(appName);
    
    // Show fallback UI
    this.showFallbackUI(appName);
    
    // Report to monitoring
    this.reportUnrecoverableError(appName);
  }
  
  showFallbackUI(appName) {
    // Show user-friendly fallback
    console.log(`Showing fallback UI for ${appName}`);
  }
  
  reportUnrecoverableError(appName) {
    // Send to error monitoring service
    console.log(`Reporting unrecoverable error for ${appName}`);
  }
}

Development vs Production Error Handling

import { addErrorHandler } from "single-spa";

const isDevelopment = process.env.NODE_ENV === "development";

if (isDevelopment) {
  // Development error handler - verbose logging
  addErrorHandler((error) => {
    console.group(`🚨 Single-SPA Error: ${error.appOrParcelName}`);
    console.error("Error:", error);
    console.error("Stack:", error.stack);
    console.error("App/Parcel:", error.appOrParcelName);
    console.error("Location:", window.location.href);
    console.error("User Agent:", navigator.userAgent);
    console.groupEnd();
    
    // Show development overlay
    showDevelopmentErrorOverlay(error);
  });
} else {
  // Production error handler - minimal logging, error reporting
  addErrorHandler((error) => {
    // Log minimal info to console
    console.error(`App ${error.appOrParcelName} failed:`, error.message);
    
    // Report to error tracking service
    if (window.errorTracker) {
      window.errorTracker.captureException(error, {
        tags: { component: error.appOrParcelName },
        level: "error"
      });
    }
    
    // Show user-friendly message
    showUserErrorMessage(error);
  });
}

function showDevelopmentErrorOverlay(error) {
  // Development-only error overlay
  const overlay = document.createElement("div");
  overlay.innerHTML = `
    <div style="position: fixed; top: 0; left: 0; right: 0; background: #ff6b6b; color: white; padding: 20px; z-index: 10000;">
      <h3>Single-SPA Error: ${error.appOrParcelName}</h3>
      <p>${error.message}</p>
      <button onclick="this.parentElement.remove()">Dismiss</button>
    </div>
  `;
  document.body.appendChild(overlay);
}

function showUserErrorMessage(error) {
  // Production user-friendly message
  console.log(`Showing user message for ${error.appOrParcelName} error`);
}

Error Prevention

import { addErrorHandler, getAppStatus } from "single-spa";

// Proactive error monitoring
class ErrorPrevention {
  constructor() {
    this.healthChecks = new Map();
    addErrorHandler(this.trackError.bind(this));
    
    // Run periodic health checks
    setInterval(this.runHealthChecks.bind(this), 30000); // Every 30 seconds
  }
  
  trackError(error) {
    const appName = error.appOrParcelName;
    const healthCheck = this.healthChecks.get(appName) || {
      errorCount: 0,
      lastError: null,
      errorTypes: new Set()
    };
    
    healthCheck.errorCount++;
    healthCheck.lastError = error;
    healthCheck.errorTypes.add(error.message);
    
    this.healthChecks.set(appName, healthCheck);
  }
  
  runHealthChecks() {
    const apps = Array.from(this.healthChecks.keys());
    
    apps.forEach(appName => {
      const health = this.healthChecks.get(appName);
      const status = getAppStatus(appName);
      
      // Check for concerning patterns
      if (health.errorCount > 5) {
        console.warn(`${appName} has ${health.errorCount} errors - investigate`);
      }
      
      if (status === "SKIP_BECAUSE_BROKEN") {
        console.error(`${appName} is broken - requires attention`);
      }
    });
  }
  
  getHealthReport() {
    const report = {};
    this.healthChecks.forEach((health, appName) => {
      report[appName] = {
        errorCount: health.errorCount,
        lastError: health.lastError?.message,
        status: getAppStatus(appName),
        errorTypes: Array.from(health.errorTypes)
      };
    });
    return report;
  }
}

Install with Tessl CLI

npx tessl i tessl/npm-single-spa

docs

application-management.md

application-status-lifecycle.md

configuration-timeouts.md

error-handling.md

index.md

navigation-routing.md

parcels-system.md

tile.json