docs/cookbook/error-handling.md
Robust error handling is essential for micro-frontend applications where multiple independent applications run within the same context. This guide covers comprehensive strategies for handling errors, implementing graceful degradation, and maintaining application stability across qiankun-based micro-frontend systems.
Micro-frontend applications face unique error scenarios:
// Error severity levels for micro-frontend applications
const ERROR_LEVELS = {
CRITICAL: 'critical', // Main app or core functionality affected
HIGH: 'high', // Major micro app functionality lost
MEDIUM: 'medium', // Partial micro app functionality affected
LOW: 'low', // Minor features or visual issues
INFO: 'info' // Non-blocking informational issues
};
const ErrorClassifier = {
classify(error, appName, context) {
// Critical: Main app crashes or core navigation fails
if (appName === 'main' || context.includes('navigation')) {
return ERROR_LEVELS.CRITICAL;
}
// High: User cannot complete primary workflows
if (context.includes('checkout') || context.includes('auth')) {
return ERROR_LEVELS.HIGH;
}
// Medium: Feature degradation but app still usable
if (error.name === 'ChunkLoadError' || error.name === 'TypeError') {
return ERROR_LEVELS.MEDIUM;
}
// Default to low for other errors
return ERROR_LEVELS.LOW;
}
};
Set up global error handlers for the entire micro-frontend ecosystem:
import { addGlobalUncaughtErrorHandler, removeGlobalUncaughtErrorHandler } from 'qiankun';
// Global error handler for all micro apps
const globalErrorHandler = (event) => {
const { error, appName, lifecycleName } = event;
console.error(`Error in micro app "${appName}" during "${lifecycleName}":`, error);
// Report to error tracking service
reportError({
error,
appName,
lifecycle: lifecycleName,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
});
// Implement recovery strategy
handleMicroAppError(appName, error, lifecycleName);
};
// Register global error handler
addGlobalUncaughtErrorHandler(globalErrorHandler);
// Remove when cleaning up (e.g., in app unmount)
// removeGlobalUncaughtErrorHandler(globalErrorHandler);
// Error handling in lifecycle hooks
const errorHandlingLifecycles = {
async beforeLoad(app) {
try {
// Pre-loading checks
const healthCheck = await fetch(`${app.entry}/health`);
if (!healthCheck.ok) {
throw new Error(`Health check failed for ${app.name}`);
}
} catch (error) {
console.warn(`Pre-load health check failed for ${app.name}:`, error);
// Continue with loading but flag as potentially unstable
markAppAsUnstable(app.name);
}
},
async beforeMount(app) {
try {
// Validate app requirements
validateAppRequirements(app);
} catch (error) {
// Attempt to fix common issues
await attemptAutoFix(app, error);
}
},
async afterMount(app) {
// Verify successful mount
setTimeout(() => {
const container = document.querySelector(app.container);
if (!container || container.children.length === 0) {
console.error(`Mount verification failed for ${app.name}`);
showFallbackContent(app.container, app.name);
}
}, 1000);
},
async beforeUnmount(app) {
try {
// Clean up resources
cleanupAppResources(app.name);
} catch (error) {
console.warn(`Cleanup error for ${app.name}:`, error);
// Force cleanup
forceCleanup(app.name);
}
}
};
// React error boundary for micro applications
import React from 'react';
class MicroAppErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
retryCount: 0,
lastRetry: null
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({
error,
errorInfo
});
// Report error
this.reportError(error, errorInfo);
// Attempt automatic recovery
this.attemptRecovery(error);
}
reportError = (error, errorInfo) => {
const errorReport = {
error: {
name: error.name,
message: error.message,
stack: error.stack
},
errorInfo,
appName: this.props.appName,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
retryCount: this.state.retryCount
};
// Send to error tracking service
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorReport)
}).catch(err => console.error('Failed to report error:', err));
};
attemptRecovery = (error) => {
const { retryCount, lastRetry } = this.state;
const now = Date.now();
// Prevent too frequent retries
if (lastRetry && now - lastRetry < 5000) {
return;
}
// Limit retry attempts
if (retryCount >= 3) {
console.error(`Max retry attempts reached for ${this.props.appName}`);
return;
}
setTimeout(() => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
retryCount: retryCount + 1,
lastRetry: now
});
}, 2000 * Math.pow(2, retryCount)); // Exponential backoff
};
render() {
if (this.state.hasError) {
const { appName, fallbackComponent: FallbackComponent } = this.props;
if (FallbackComponent) {
return (
<FallbackComponent
error={this.state.error}
appName={appName}
onRetry={() => this.attemptRecovery(this.state.error)}
/>
);
}
return (
<div className="micro-app-error">
<h3>Application Error</h3>
<p>The {appName} application encountered an error.</p>
<button onClick={() => this.attemptRecovery(this.state.error)}>
Retry ({this.state.retryCount}/3)
</button>
<details style={{ marginTop: '1rem' }}>
<summary>Error Details</summary>
<pre>{this.state.error?.stack}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
// Usage with micro app
function MicroAppContainer({ appName, entry }) {
return (
<MicroAppErrorBoundary
appName={appName}
fallbackComponent={CustomErrorFallback}
>
<div id={`${appName}-container`} />
</MicroAppErrorBoundary>
);
}
// Vue global error handler for micro apps
const app = createApp(MainApp);
app.config.errorHandler = (err, instance, info) => {
const appName = instance?.$root?.$options?.name || 'unknown';
console.error(`Vue error in ${appName}:`, err, info);
// Report error
reportVueError({
error: err,
appName,
info,
timestamp: Date.now()
});
// Attempt recovery
if (instance && typeof instance.$forceUpdate === 'function') {
instance.$forceUpdate();
}
};
// Vue 2 error boundary component
Vue.component('ErrorBoundary', {
data() {
return {
hasError: false,
error: null
};
},
errorCaptured(err, instance, info) {
this.hasError = true;
this.error = err;
// Report error
this.reportError(err, info);
// Prevent error from propagating
return false;
},
methods: {
reportError(error, info) {
// Error reporting logic
},
retry() {
this.hasError = false;
this.error = null;
this.$forceUpdate();
}
},
render(h) {
if (this.hasError) {
return h('div', { class: 'error-boundary' }, [
h('h3', 'Something went wrong'),
h('button', { on: { click: this.retry } }, 'Retry'),
h('pre', this.error?.message)
]);
}
return this.$slots.default;
}
});
// Progressive feature loading with fallbacks
class FeatureLoader {
constructor() {
this.features = new Map();
this.fallbacks = new Map();
}
register(featureName, loader, fallback) {
this.features.set(featureName, loader);
this.fallbacks.set(featureName, fallback);
}
async load(featureName) {
try {
const loader = this.features.get(featureName);
if (!loader) {
throw new Error(`Feature "${featureName}" not registered`);
}
const feature = await loader();
return feature;
} catch (error) {
console.warn(`Failed to load feature "${featureName}":`, error);
const fallback = this.fallbacks.get(featureName);
if (fallback) {
return await fallback();
}
throw error;
}
}
}
// Usage example
const featureLoader = new FeatureLoader();
// Register advanced dashboard with fallback
featureLoader.register(
'advanced-dashboard',
() => import('./AdvancedDashboard'),
() => import('./BasicDashboard')
);
// Register chart component with static fallback
featureLoader.register(
'interactive-charts',
() => import('./InteractiveCharts'),
() => Promise.resolve(() => '<div>Charts unavailable</div>')
);
// Comprehensive fallback components
const ErrorFallbacks = {
// Network error fallback
NetworkError: ({ onRetry, appName }) => (
<div className="error-fallback network-error">
<div className="error-icon">🌐</div>
<h3>Connection Problem</h3>
<p>Unable to load {appName}. Please check your internet connection.</p>
<div className="error-actions">
<button onClick={onRetry} className="retry-button">
Try Again
</button>
<button onClick={() => window.location.reload()}>
Refresh Page
</button>
</div>
</div>
),
// JavaScript error fallback
JavaScriptError: ({ error, appName, onRetry }) => (
<div className="error-fallback js-error">
<div className="error-icon">⚠️</div>
<h3>Application Error</h3>
<p>The {appName} application encountered a technical issue.</p>
<div className="error-actions">
<button onClick={onRetry} className="retry-button">
Reload Application
</button>
<button onClick={() => reportIssue(error, appName)}>
Report Issue
</button>
</div>
{process.env.NODE_ENV === 'development' && (
<details className="error-details">
<summary>Technical Details</summary>
<pre>{error.stack}</pre>
</details>
)}
</div>
),
// Loading timeout fallback
LoadingTimeout: ({ appName, onRetry }) => (
<div className="error-fallback loading-timeout">
<div className="error-icon">⏱️</div>
<h3>Loading Timeout</h3>
<p>{appName} is taking longer than expected to load.</p>
<div className="error-actions">
<button onClick={onRetry} className="retry-button">
Try Again
</button>
<button onClick={() => loadBasicVersion(appName)}>
Load Basic Version
</button>
</div>
</div>
),
// Generic fallback
Generic: ({ error, appName, onRetry }) => (
<div className="error-fallback generic">
<div className="error-icon">🔧</div>
<h3>Temporary Issue</h3>
<p>We're experiencing technical difficulties with {appName}.</p>
<div className="error-actions">
<button onClick={onRetry} className="retry-button">
Try Again
</button>
</div>
</div>
)
};
// Circuit breaker for micro app loading
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000, monitor = 30000) {
this.failureThreshold = threshold;
this.timeout = timeout;
this.monitoringPeriod = monitor;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.lastFailureTime = null;
this.nextAttemptTime = null;
}
async execute(operation, appName) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttemptTime) {
throw new Error(`Circuit breaker is OPEN for ${appName}`);
}
this.state = 'HALF_OPEN';
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttemptTime = Date.now() + this.timeout;
}
}
getState() {
return this.state;
}
}
// Usage with micro app loading
const circuitBreakers = new Map();
const loadMicroAppWithCircuitBreaker = async (appConfig) => {
const { name } = appConfig;
if (!circuitBreakers.has(name)) {
circuitBreakers.set(name, new CircuitBreaker());
}
const breaker = circuitBreakers.get(name);
try {
return await breaker.execute(() => loadMicroApp(appConfig), name);
} catch (error) {
console.error(`Circuit breaker prevented loading ${name}:`, error);
throw error;
}
};
// Advanced error tracking system
class ErrorTracker {
constructor(config) {
this.config = {
endpoint: '/api/errors',
batchSize: 10,
batchTimeout: 5000,
maxRetries: 3,
...config
};
this.errorQueue = [];
this.batchTimeout = null;
this.retryCount = new Map();
}
track(error, context = {}) {
const errorData = this.serializeError(error, context);
// Add to queue
this.errorQueue.push(errorData);
// Process batch if queue is full
if (this.errorQueue.length >= this.config.batchSize) {
this.processBatch();
} else {
// Set timeout for batch processing
this.scheduleBatchProcessing();
}
}
serializeError(error, context) {
return {
id: this.generateErrorId(),
timestamp: Date.now(),
error: {
name: error.name,
message: error.message,
stack: error.stack,
fileName: error.fileName,
lineNumber: error.lineNumber,
columnNumber: error.columnNumber
},
context: {
appName: context.appName || 'unknown',
userId: context.userId,
sessionId: this.getSessionId(),
url: window.location.href,
userAgent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
...context
},
environment: {
isDevelopment: process.env.NODE_ENV === 'development',
timestamp: Date.now(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
};
}
scheduleBatchProcessing() {
if (this.batchTimeout) {
clearTimeout(this.batchTimeout);
}
this.batchTimeout = setTimeout(() => {
this.processBatch();
}, this.config.batchTimeout);
}
async processBatch() {
if (this.errorQueue.length === 0) return;
const batch = this.errorQueue.splice(0, this.config.batchSize);
try {
await this.sendErrors(batch);
// Clear retry count on success
batch.forEach(error => {
this.retryCount.delete(error.id);
});
} catch (error) {
console.error('Failed to send error batch:', error);
// Retry logic
batch.forEach(errorData => {
const retries = this.retryCount.get(errorData.id) || 0;
if (retries < this.config.maxRetries) {
this.retryCount.set(errorData.id, retries + 1);
this.errorQueue.unshift(errorData); // Add back to front of queue
}
});
}
}
async sendErrors(errors) {
const response = await fetch(this.config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ errors })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
generateErrorId() {
return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
getSessionId() {
// Implementation to get/generate session ID
return sessionStorage.getItem('sessionId') || 'anonymous';
}
}
// Initialize global error tracker
const errorTracker = new ErrorTracker();
// Track unhandled errors
window.addEventListener('error', (event) => {
errorTracker.track(event.error, {
type: 'unhandled_error',
source: 'window.onerror'
});
});
// Track unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
errorTracker.track(event.reason, {
type: 'unhandled_rejection',
source: 'unhandledrejection'
});
});
// Monitor error impact on performance
class ErrorImpactMonitor {
constructor() {
this.errorImpacts = new Map();
this.performanceBaseline = this.measureBaseline();
}
measureBaseline() {
return {
loadTime: performance.now(),
memoryUsage: performance.memory ? performance.memory.usedJSHeapSize : 0,
timing: performance.timing
};
}
recordErrorImpact(errorId, appName) {
const impact = {
errorId,
appName,
timestamp: Date.now(),
performance: {
loadTime: performance.now(),
memoryUsage: performance.memory ? performance.memory.usedJSHeapSize : 0,
timing: performance.timing
},
userExperience: {
pageVisible: !document.hidden,
userActive: this.isUserActive(),
scrollPosition: window.scrollY
}
};
this.errorImpacts.set(errorId, impact);
this.analyzeImpact(impact);
}
analyzeImpact(impact) {
const { performance: current } = impact;
const baseline = this.performanceBaseline;
const memoryIncrease = current.memoryUsage - baseline.memoryUsage;
const loadTimeIncrease = current.loadTime - baseline.loadTime;
if (memoryIncrease > 50 * 1024 * 1024) { // 50MB
console.warn('High memory impact detected after error:', impact);
}
if (loadTimeIncrease > 5000) { // 5 seconds
console.warn('Significant performance degradation after error:', impact);
}
}
isUserActive() {
// Simple user activity detection
return Date.now() - this.lastUserActivity < 30000;
}
}
// Comprehensive recovery system
class RecoveryManager {
constructor() {
this.recoveryStrategies = new Map();
this.setupDefaultStrategies();
}
setupDefaultStrategies() {
// Network error recovery
this.register('NetworkError', async (error, context) => {
await this.waitForConnection();
return this.reloadMicroApp(context.appName);
});
// Chunk load error recovery
this.register('ChunkLoadError', async (error, context) => {
// Clear webpack cache
if (window.__webpack_require__ && window.__webpack_require__.cache) {
delete window.__webpack_require__.cache[error.request];
}
// Reload with cache busting
return this.reloadWithCacheBust(context.appName);
});
// Script error recovery
this.register('TypeError', async (error, context) => {
// Attempt to reload dependencies
await this.reloadDependencies(context.appName);
return this.remountMicroApp(context.appName);
});
// Memory error recovery
this.register('RangeError', async (error, context) => {
// Force garbage collection
if (window.gc) window.gc();
// Reduce memory footprint
await this.reducememoryFootprint(context.appName);
return this.reloadMicroApp(context.appName);
});
}
register(errorType, strategy) {
this.recoveryStrategies.set(errorType, strategy);
}
async recover(error, context) {
const strategy = this.recoveryStrategies.get(error.name);
if (strategy) {
try {
console.log(`Attempting recovery for ${error.name} in ${context.appName}`);
const result = await strategy(error, context);
console.log(`Recovery successful for ${context.appName}`);
return result;
} catch (recoveryError) {
console.error(`Recovery failed for ${context.appName}:`, recoveryError);
return this.fallbackRecovery(context);
}
}
return this.fallbackRecovery(context);
}
async waitForConnection() {
return new Promise((resolve) => {
if (navigator.onLine) {
resolve();
} else {
const handleOnline = () => {
window.removeEventListener('online', handleOnline);
resolve();
};
window.addEventListener('online', handleOnline);
}
});
}
async reloadMicroApp(appName) {
// Unmount current instance
try {
await unmountMicroApp(appName);
} catch (error) {
console.warn(`Failed to unmount ${appName}:`, error);
}
// Reload the micro app
const appConfig = getAppConfig(appName);
return loadMicroApp(appConfig);
}
async reloadWithCacheBust(appName) {
const appConfig = getAppConfig(appName);
const cacheBustEntry = `${appConfig.entry}?t=${Date.now()}`;
return loadMicroApp({
...appConfig,
entry: cacheBustEntry
});
}
async fallbackRecovery(context) {
console.log(`Using fallback recovery for ${context.appName}`);
// Show fallback UI
showFallbackUI(context.appName);
// Report recovery failure
reportRecoveryFailure(context);
return null;
}
}
// User-controlled recovery interface
const RecoveryPanel = ({ appName, error, onRecover, onDismiss }) => {
const [recovering, setRecovering] = useState(false);
const [lastAttempt, setLastAttempt] = useState(null);
const handleRecover = async (strategy) => {
setRecovering(true);
setLastAttempt(Date.now());
try {
await onRecover(strategy);
} catch (error) {
console.error('User-initiated recovery failed:', error);
} finally {
setRecovering(false);
}
};
const recoveryOptions = [
{
key: 'reload',
label: 'Reload Application',
description: 'Restart the application from scratch',
action: () => handleRecover('reload')
},
{
key: 'reset',
label: 'Reset to Default',
description: 'Clear all data and reload',
action: () => handleRecover('reset')
},
{
key: 'safe-mode',
label: 'Safe Mode',
description: 'Load with minimal features',
action: () => handleRecover('safe-mode')
}
];
return (
<div className="recovery-panel">
<div className="recovery-header">
<h3>Recovery Options for {appName}</h3>
<button onClick={onDismiss} className="close-button">×</button>
</div>
<div className="error-summary">
<p><strong>Error:</strong> {error.message}</p>
{lastAttempt && (
<p><small>Last attempt: {new Date(lastAttempt).toLocaleTimeString()}</small></p>
)}
</div>
<div className="recovery-options">
{recoveryOptions.map(option => (
<button
key={option.key}
onClick={option.action}
disabled={recovering}
className="recovery-option"
>
<div className="option-label">{option.label}</div>
<div className="option-description">{option.description}</div>
</button>
))}
</div>
{recovering && (
<div className="recovery-progress">
<div className="spinner" />
<span>Attempting recovery...</span>
</div>
)}
</div>
);
};
// Comprehensive error handling checklist
const errorHandlingChecklist = {
prevention: {
validation: '✓ Input validation implemented',
typeChecking: '✓ TypeScript or PropTypes used',
testing: '✓ Error scenarios tested',
monitoring: '✓ Health checks in place'
},
detection: {
globalHandlers: '✓ Global error handlers set up',
boundaries: '✓ Error boundaries implemented',
logging: '✓ Comprehensive error logging',
alerting: '✓ Real-time error alerts'
},
recovery: {
gracefulDegradation: '✓ Fallback UIs implemented',
automaticRecovery: '✓ Auto-recovery strategies',
userRecovery: '✓ User-initiated recovery options',
resourceCleanup: '✓ Proper cleanup on errors'
},
learning: {
errorTracking: '✓ Error analytics in place',
trendAnalysis: '✓ Error trend monitoring',
rootCauseAnalysis: '✓ RCA process defined',
continuousImprovement: '✓ Regular error review meetings'
}
};