PatternsMicrofrontends
Error Boundaries Between Microfrontends
Isolate failures and prevent cascade errors across microfrontends
In microfrontend architecture, one failing MFE shouldn't crash the entire application. This guide covers error isolation and recovery patterns.
π― The Problem
Without error boundaries:
ββββββββββββββββββββββββββββββββ
β Shell App β
β ββββββββββ ββββββββββ β
β βHeader β βProductsβ β β
β β OK β β CRASH β β
β ββββββββββ ββββββββββ β
β β
β π₯ Entire app crashes! β
ββββββββββββββββββββββββββββββββ
With error boundaries:
ββββββββββββββββββββββββββββββββ
β Shell App β
β ββββββββββ ββββββββββ β
β βHeader β βProductsβ β β
β β OK β βFallbackβ β
β ββββββββββ ββββββββββ β
β β
β β
App still works! β
ββββββββββββββββββββββββββββββββπ‘οΈ Pattern 1: React Error Boundaries
Catch errors in React components.
Basic Implementation
// Shell App
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
this.props.onError?.(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
export default ErrorBoundary;Usage in Shell
// Shell App
import ErrorBoundary from './ErrorBoundary';
export function Shell() {
return (
<div>
{/* Header with error boundary */}
<ErrorBoundary fallback={<HeaderFallback />}>
<HeaderMFE />
</ErrorBoundary>
{/* Products with error boundary */}
<ErrorBoundary fallback={<ProductsFallback />}>
<ProductsMFE />
</ErrorBoundary>
{/* Checkout with error boundary */}
<ErrorBoundary fallback={<CheckoutFallback />}>
<CheckoutMFE />
</ErrorBoundary>
</div>
);
}π Pattern 2: Error Boundary with Retry
Allow users to retry failed MFEs.
class ErrorBoundaryWithRetry extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null, retryCount: 0 };
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
resetErrorBoundary = () => {
this.setState({
hasError: false,
error: null,
retryCount: this.state.retryCount + 1
});
};
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.resetErrorBoundary}>
Try Again ({this.state.retryCount} retries)
</button>
</div>
);
}
return this.props.children;
}
}π‘ Pattern 3: Error Reporting
Send errors to monitoring service.
// shared/error-reporter.ts
interface ErrorReport {
mfeName: string;
error: Error;
errorInfo: React.ErrorInfo;
userId?: string;
timestamp: number;
url: string;
userAgent: string;
}
class ErrorReporter {
report(report: ErrorReport) {
// Send to Sentry, Datadog, etc
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(report)
});
// Also log to console in dev
if (process.env.NODE_ENV === 'development') {
console.error('Error Report:', report);
}
}
reportMFEError(mfeName: string, error: Error, errorInfo: React.ErrorInfo) {
this.report({
mfeName,
error,
errorInfo,
userId: getCurrentUserId(),
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
});
}
}
export const errorReporter = new ErrorReporter();Usage
<ErrorBoundary
fallback={<ProductsFallback />}
onError={(error, errorInfo) => {
errorReporter.reportMFEError('products', error, errorInfo);
}}
>
<ProductsMFE />
</ErrorBoundary>π Pattern 4: Lazy Loading with Error Handling
Handle MFE load failures.
import { lazy, Suspense } from 'react';
// Lazy load with retry
function lazyWithRetry(importFn: () => Promise<any>, retries = 3) {
return lazy(async () => {
for (let i = 0; i < retries; i++) {
try {
return await importFn();
} catch (error) {
if (i === retries - 1) {
throw error;
}
// Wait before retry (exponential backoff)
await new Promise(resolve =>
setTimeout(resolve, 1000 * Math.pow(2, i))
);
}
}
throw new Error('Failed to load module');
});
}
// Usage
const ProductsMFE = lazyWithRetry(
() => import('products/App')
);
export function Shell() {
return (
<ErrorBoundary
fallback={<div>Failed to load Products. <button onClick={reload}>Reload</button></div>}
>
<Suspense fallback={<div>Loading Products...</div>}>
<ProductsMFE />
</Suspense>
</ErrorBoundary>
);
}π¨ Pattern 5: Graceful Degradation
Show simplified UI when MFE fails.
// Shell App
function ProductsSection() {
const [hasError, setHasError] = useState(false);
if (hasError) {
// Show simplified fallback
return (
<div className="products-fallback">
<h2>Products</h2>
<p>Our product catalog is temporarily unavailable.</p>
<a href="/products">View full catalog β</a>
</div>
);
}
return (
<ErrorBoundary
fallback={null}
onError={() => setHasError(true)}
>
<ProductsMFE />
</ErrorBoundary>
);
}π¨ Pattern 6: Circuit Breaker
Stop loading broken MFEs temporarily.
// shared/circuit-breaker.ts
class CircuitBreaker {
private failures = new Map<string, number>();
private lastFailure = new Map<string, number>();
private threshold = 5; // failures
private timeout = 60000; // 1 minute
recordFailure(mfeName: string) {
const count = this.failures.get(mfeName) || 0;
this.failures.set(mfeName, count + 1);
this.lastFailure.set(mfeName, Date.now());
}
recordSuccess(mfeName: string) {
this.failures.set(mfeName, 0);
}
isOpen(mfeName: string): boolean {
const failures = this.failures.get(mfeName) || 0;
const lastFail = this.lastFailure.get(mfeName) || 0;
// Reset after timeout
if (Date.now() - lastFail > this.timeout) {
this.failures.set(mfeName, 0);
return false;
}
return failures >= this.threshold;
}
}
export const circuitBreaker = new CircuitBreaker();Usage
function ProductsSection() {
if (circuitBreaker.isOpen('products')) {
return <ProductsFallback message="Service temporarily unavailable" />;
}
return (
<ErrorBoundary
fallback={<ProductsFallback />}
onError={() => {
circuitBreaker.recordFailure('products');
}}
>
<ProductsMFE
onLoad={() => circuitBreaker.recordSuccess('products')}
/>
</ErrorBoundary>
);
}π Pattern 7: Error Metrics Dashboard
Monitor MFE health.
// shared/error-metrics.ts
interface ErrorMetric {
mfeName: string;
errorCount: number;
lastError: Date;
errorRate: number;
}
class ErrorMetrics {
private metrics = new Map<string, ErrorMetric>();
recordError(mfeName: string) {
const current = this.metrics.get(mfeName) || {
mfeName,
errorCount: 0,
lastError: new Date(),
errorRate: 0,
};
current.errorCount++;
current.lastError = new Date();
this.metrics.set(mfeName, current);
// Send to monitoring service
this.sendMetrics(current);
}
private sendMetrics(metric: ErrorMetric) {
fetch('/api/metrics/errors', {
method: 'POST',
body: JSON.stringify(metric)
});
}
getMetrics(): ErrorMetric[] {
return Array.from(this.metrics.values());
}
}
export const errorMetrics = new ErrorMetrics();π Pattern 8: Async Error Handling
Handle errors outside React tree.
// Shell App - Global error handler
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
// Determine which MFE caused error
const mfeName = determineMFEFromError(event.error);
if (mfeName) {
errorReporter.reportMFEError(mfeName, event.error, {});
errorMetrics.recordError(mfeName);
}
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
const mfeName = determineMFEFromError(event.reason);
if (mfeName) {
errorReporter.reportMFEError(mfeName, event.reason, {});
}
});
function determineMFEFromError(error: Error): string | null {
const stack = error.stack || '';
if (stack.includes('/header/')) return 'header';
if (stack.includes('/products/')) return 'products';
if (stack.includes('/checkout/')) return 'checkout';
return null;
}π Best Practices
1. One Boundary Per MFE
// β
GOOD: Isolate each MFE
<ErrorBoundary><HeaderMFE /></ErrorBoundary>
<ErrorBoundary><ProductsMFE /></ErrorBoundary>
<ErrorBoundary><CheckoutMFE /></ErrorBoundary>
// β BAD: One boundary for all
<ErrorBoundary>
<HeaderMFE />
<ProductsMFE />
<CheckoutMFE />
</ErrorBoundary>2. Meaningful Fallbacks
// β
GOOD: Specific fallback
<ErrorBoundary fallback={
<div>
<h2>Products temporarily unavailable</h2>
<p>We're working on it. Try again in a few minutes.</p>
<button onClick={retry}>Retry</button>
</div>
}>
// β BAD: Generic fallback
<ErrorBoundary fallback={<div>Error</div>}>3. Report All Errors
// Always report to monitoring
<ErrorBoundary
fallback={<Fallback />}
onError={(error, info) => {
errorReporter.report(error, info);
errorMetrics.recordError(mfeName);
}}
>π’ Real-World Examples
Spotify
// Error boundary per widget
// Circuit breaker for failing services
// Graceful degradation everywhere
// Real-time error monitoringAmazon
// Independent error boundaries
// Fallback to cached content
// Silent failures when possible
// Detailed error reportingNetflix
// Error boundaries with retry
// Circuit breaker pattern
// A/B test error UIs
// Automated rollback on error spikeπ Key Takeaways
- One boundary per MFE - Isolate failures
- Meaningful fallbacks - Help users understand
- Report everything - Monitor and alert
- Circuit breaker - Stop retrying broken MFEs
- Retry with backoff - Network issues are common
- Test failures - Simulate MFE crashes
- Graceful degradation - App should still work
Errors will happen. Plan for them from day one.