The router for easy microfrontends that enables multiple frameworks to coexist on the same page.
—
Global error handling system for managing application errors and failures in microfrontend architectures.
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);
}
});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);
}
}
}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);
}
}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}`);
}
}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`);
}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