📅 NGÀY 50: Error Boundaries
🎯 Mục tiêu học tập (5 phút)
- [ ] Hiểu Error Boundary concept và khi nào cần dùng
- [ ] Biết cách implement Error Boundary (class component pattern)
- [ ] Sử dụng react-error-boundary library hiệu quả
- [ ] Thiết kế error recovery strategies
- [ ] Kết hợp Error Boundaries với Suspense cho complete async handling
🤔 Kiểm tra đầu vào (5 phút)
- Suspense boundaries hoạt động như thế nào?
- Thứ tự đúng: Error Boundary và Suspense?
- Tại sao Error Boundary bọc ngoài Suspense?
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
Khi component throw error, điều gì xảy ra?
// ❌ Component throw error → Toàn bộ app crash!
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetchProducts()
.then((data) => setProducts(data.products)) // Assume data.products exists
.catch((err) => console.error(err)); // Log error nhưng...
}, []);
// 💥 Nếu data structure khác (data.items instead of data.products)
// products là undefined → .map() throws error → WHITE SCREEN OF DEATH!
return (
<div>
{products.map(
(
product, // 💥 CRASH!
) => (
<ProductCard
key={product.id}
product={product}
/>
),
)}
</div>
);
}Real-world scenario:
// Dashboard với multiple widgets
function Dashboard() {
return (
<div>
<UserWidget /> {/* Works fine */}
<StatsWidget /> {/* Works fine */}
<ChartWidget /> {/* 💥 Throws error - API changed */}
<ActivityWidget /> {/* Never renders - app crashed */}
</div>
);
}
// Vấn đề:
// 1. Một widget lỗi → Toàn bộ dashboard crash
// 2. User thấy blank screen - Terrible UX!
// 3. Không có cách recovery - Must refresh page
// 4. Mất tất cả state - User frustrated!Without Error Boundary:
Component Error
↓
React unmounts entire tree
↓
White screen
↓
User confused
↓
Refresh page
↓
Lose all state1.2 Giải Pháp: Error Boundaries
Error Boundary = try-catch cho React components
// ✅ Error Boundary giữ app hoạt động
<ErrorBoundary fallback={<div>Widget failed to load</div>}>
<ChartWidget />
</ErrorBoundary>
// Flow:
// ChartWidget throws error
// ↓
// ErrorBoundary catches
// ↓
// Show fallback UI (Widget failed)
// ↓
// Other widgets still work!
// ↓
// User can continue using appBenefits:
- Graceful degradation - App continues working
- Isolated failures - One component fails ≠ App fails
- Better UX - Show error message instead of blank screen
- Recovery options - Provide retry button
- Error logging - Track errors in production
1.3 Mental Model
Error Boundary như circuit breaker:
Normal Flow:
Parent → Child → GrandChild (all render fine)
With Error:
Parent → Child → GrandChild 💥
↓
ErrorBoundary catches
↓
Show fallback instead of GrandChild
↓
Parent & Child still render fineAnalogy: Building Fire Doors
Without Error Boundary:
Fire in Room A → Spreads to entire building → Total loss
With Error Boundary:
Fire in Room A → Fire door closes → Rest of building safeHierarchy:
<App> // ← Top-level error boundary
<ErrorBoundary fallback={...}>
<Dashboard> // ← Feature-level error boundary
<ErrorBoundary fallback={...}>
<Widget /> // ← Component that might fail
</ErrorBoundary>
</Dashboard>
</ErrorBoundary>
</App>1.4 Hiểu Lầm Phổ Biến
❌ Hiểu lầm 1: "Error Boundary catches tất cả errors"
// ❌ Error Boundary KHÔNG catch:
// 1. Event handlers
function Button() {
const handleClick = () => {
throw new Error('Click error'); // NOT caught by Error Boundary!
};
return <button onClick={handleClick}>Click</button>;
}
// Fix: Dùng try-catch thường
function Button() {
const handleClick = () => {
try {
riskyOperation();
} catch (error) {
console.error(error);
showToast('Error occurred');
}
};
return <button onClick={handleClick}>Click</button>;
}
// 2. Async code (setTimeout, Promises)
function Component() {
useEffect(() => {
setTimeout(() => {
throw new Error('Timeout error'); // NOT caught!
}, 1000);
}, []);
}
// Fix: Handle trong async code
function Component() {
useEffect(() => {
setTimeout(() => {
try {
riskyOperation();
} catch (error) {
console.error(error);
}
}, 1000);
}, []);
}
// 3. Server-side rendering errors
// 4. Errors trong Error Boundary chính nó❌ Hiểu lầm 2: "Error Boundary phải là class component"
// ✅ ĐÚNG (hiện tại): Error Boundary phải là class
class ErrorBoundary extends Component {
// ...
}
// ⚠️ React team đang làm hooks cho Error Boundary
// Tương lai có thể có: useErrorBoundary()
// Nhưng hiện tại (2025) chưa có
// 💡 Giải pháp: Dùng library react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
// → Wrapper xung quanh class component, API như function component❌ Hiểu lầm 3: "Một Error Boundary cho toàn bộ app là đủ"
// ❌ BAD: Single top-level error boundary
<ErrorBoundary>
<EntireApp />
</ErrorBoundary>
// Vấn đề:
// - Bất kỳ error nào → Toàn bộ app unmount
// - Không tốt hơn là không có error boundary!
// ✅ GOOD: Multiple strategic boundaries
<App>
<ErrorBoundary> {/* Top-level: Catch catastrophic errors */}
<Header />
<ErrorBoundary> {/* Feature-level: Isolate features */}
<Sidebar />
</ErrorBoundary>
<ErrorBoundary> {/* Feature-level */}
<MainContent>
<ErrorBoundary> {/* Component-level: Isolate widgets */}
<CriticalWidget />
</ErrorBoundary>
</MainContent>
</ErrorBoundary>
</ErrorBoundary>
</App>💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Class-based Error Boundary (Legacy Pattern) ⭐
/**
* Demo: Basic Error Boundary với class component
*
* ⚠️ LƯU Ý: Hiện tại (2025), Error Boundary PHẢI là class component
* React chưa có hooks cho error boundaries
* Demo này để HIỂU cơ chế - Production nên dùng react-error-boundary library
*/
import { Component } from 'react';
// ✅ Basic Error Boundary
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// Called when child component throws error during render
static getDerivedStateFromError(error) {
// Update state để next render show fallback UI
return { hasError: true };
}
// Called after error is caught - for logging
componentDidCatch(error, errorInfo) {
// errorInfo.componentStack: Stack trace của component tree
console.error('Error caught by boundary:', error);
console.error('Component stack:', errorInfo.componentStack);
// TODO: Send to error tracking service (Sentry, LogRocket, etc.)
// logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// Fallback UI
return (
<div style={{ padding: 20, border: '2px solid red', borderRadius: 8 }}>
<h2>⚠️ Something went wrong</h2>
<p>We're sorry for the inconvenience.</p>
</div>
);
}
return this.props.children;
}
}
// Component that throws error
function BuggyComponent() {
const [count, setCount] = useState(0);
if (count === 3) {
// Simulate error at specific count
throw new Error('I crashed at count 3!');
}
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment (crash at 3)
</button>
</div>
);
}
// Usage
function App() {
return (
<div>
<h1>Error Boundary Demo</h1>
<ErrorBoundary>
<BuggyComponent />
</ErrorBoundary>
<div style={{ marginTop: 20, padding: 10, backgroundColor: '#f0f0f0' }}>
<p>This section is outside the error boundary</p>
<p>It will still render even if BuggyComponent crashes!</p>
</div>
</div>
);
}
// Kết quả:
// 1. Click button 3 lần
// 2. Component throws error
// 3. Error Boundary catches → Show fallback UI
// 4. Bottom section still renders normallyDemo 2: Error Boundary với Retry ⭐⭐
/**
* Demo: Error Boundary với retry mechanism
* Production pattern - cho phép user recover từ errors
*/
class ErrorBoundaryWithRetry extends Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
};
}
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
componentDidCatch(error, errorInfo) {
console.error('Error:', error);
console.error('Error Info:', errorInfo);
}
// Reset error state → Trigger re-render of children
handleReset = () => {
this.setState({
hasError: false,
error: null,
});
};
render() {
if (this.state.hasError) {
return (
<div
style={{
padding: 24,
border: '2px solid #ef4444',
borderRadius: 8,
backgroundColor: '#fee',
textAlign: 'center',
}}
>
<h2 style={{ color: '#dc2626', marginBottom: 12 }}>
⚠️ Error Occurred
</h2>
<p style={{ color: '#991b1b', marginBottom: 8 }}>
{this.state.error?.message || 'Unknown error'}
</p>
<button
onClick={this.handleReset}
style={{
padding: '10px 20px',
backgroundColor: '#ef4444',
color: 'white',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
fontWeight: 600,
}}
>
🔄 Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// Simulated async component with random failures
function UnstableWidget() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
// Simulate API call with 40% failure rate
setTimeout(() => {
if (Math.random() < 0.4) {
throw new Error('API call failed');
}
setData({ message: 'Data loaded successfully!' });
setLoading(false);
}, 1000);
}, []); // Empty deps - only run once per mount
if (loading) return <div>Loading widget...</div>;
return (
<div style={{ padding: 16, backgroundColor: '#dff0d8', borderRadius: 8 }}>
<h3>✅ Widget Loaded</h3>
<p>{data.message}</p>
</div>
);
}
function App() {
return (
<div style={{ padding: 20 }}>
<h1>Error Boundary with Retry</h1>
<ErrorBoundaryWithRetry>
<UnstableWidget />
</ErrorBoundaryWithRetry>
<p style={{ marginTop: 20, color: '#666', fontSize: 14 }}>
💡 Widget has 40% failure rate. Click "Try Again" if it fails.
</p>
</div>
);
}
// Flow:
// 1. UnstableWidget mounts → Fetch data
// 2. If success → Show data
// 3. If error → Error Boundary catches → Show error UI
// 4. User clicks "Try Again" → Reset state → Re-mount component → Try againDemo 3: react-error-boundary Library ⭐⭐⭐
/**
* Demo: Modern Error Boundary với react-error-boundary
*
* ⚠️ QUAN TRỌNG: Trong real project, LUÔN dùng library này
* Không tự implement class component Error Boundary
*
* Library cung cấp:
* - Function component API (easier)
* - useErrorHandler hook
* - Reset keys (auto reset when props change)
* - Better TypeScript support
*/
// npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
// ============= FALLBACK COMPONENTS =============
/**
* Simple fallback
*/
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div
role='alert'
style={{
padding: 24,
border: '2px solid #ef4444',
borderRadius: 8,
backgroundColor: '#fee',
}}
>
<h2 style={{ color: '#dc2626' }}>⚠️ Something went wrong</h2>
<pre
style={{
color: '#991b1b',
fontSize: 12,
backgroundColor: '#fecaca',
padding: 12,
borderRadius: 4,
overflow: 'auto',
}}
>
{error.message}
</pre>
<button
onClick={resetErrorBoundary}
style={{
marginTop: 12,
padding: '10px 20px',
backgroundColor: '#ef4444',
color: 'white',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
}}
>
Try Again
</button>
</div>
);
}
/**
* Custom fallback cho specific use case
*/
function WidgetErrorFallback({ error, resetErrorBoundary }) {
return (
<div
style={{
padding: 16,
border: '1px dashed #dc2626',
borderRadius: 8,
textAlign: 'center',
backgroundColor: '#fef2f2',
}}
>
<div style={{ fontSize: 32, marginBottom: 8 }}>😞</div>
<p style={{ fontSize: 14, color: '#991b1b', marginBottom: 12 }}>
Widget failed to load
</p>
<button
onClick={resetErrorBoundary}
style={{
padding: '6px 12px',
fontSize: 12,
backgroundColor: '#dc2626',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
}}
>
Retry
</button>
</div>
);
}
// ============= COMPONENTS =============
function RandomFailWidget({ name }) {
const shouldFail = Math.random() < 0.3;
if (shouldFail) {
throw new Error(`${name} failed to load`);
}
return (
<div
style={{
padding: 16,
backgroundColor: '#d1fae5',
borderRadius: 8,
border: '1px solid #10b981',
}}
>
<h3 style={{ margin: 0, fontSize: 16 }}>✅ {name}</h3>
<p style={{ margin: '8px 0 0', fontSize: 14, color: '#065f46' }}>
Widget loaded successfully
</p>
</div>
);
}
// ============= USAGE PATTERNS =============
function App() {
const [resetKey, setResetKey] = useState(0);
// Error handler callback
const handleError = (error, errorInfo) => {
console.error('Caught error:', error);
console.error('Error info:', errorInfo);
// TODO: Log to error tracking service
};
// Reset handler
const handleReset = () => {
setResetKey((prev) => prev + 1);
console.log('Error boundary reset');
};
return (
<div style={{ padding: 20 }}>
<h1>react-error-boundary Demo</h1>
{/* Pattern 1: Basic usage */}
<div style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 18 }}>Pattern 1: Basic</h2>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={handleError}
onReset={handleReset}
>
<RandomFailWidget name='Widget A' />
</ErrorBoundary>
</div>
{/* Pattern 2: Custom fallback */}
<div style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 18 }}>Pattern 2: Custom Fallback</h2>
<ErrorBoundary
FallbackComponent={WidgetErrorFallback}
onError={handleError}
>
<RandomFailWidget name='Widget B' />
</ErrorBoundary>
</div>
{/* Pattern 3: Reset keys - Auto reset khi key changes */}
<div style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 18 }}>Pattern 3: Reset Keys</h2>
<button
onClick={() => setResetKey((prev) => prev + 1)}
style={{
marginBottom: 12,
padding: '8px 16px',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
}}
>
Reset All (Key: {resetKey})
</button>
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[resetKey]} // Auto reset when resetKey changes
>
<RandomFailWidget name='Widget C' />
</ErrorBoundary>
</div>
{/* Pattern 4: Inline fallback render */}
<div style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 18 }}>Pattern 4: Inline Fallback</h2>
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div
style={{ padding: 16, backgroundColor: '#fee', borderRadius: 8 }}
>
<p style={{ color: '#dc2626' }}>Inline error: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<RandomFailWidget name='Widget D' />
</ErrorBoundary>
</div>
<p style={{ fontSize: 14, color: '#666', marginTop: 24 }}>
💡 Each widget has 30% failure rate. Refresh or retry if errors appear.
</p>
</div>
);
}
// Advantages của react-error-boundary:
// ✅ No class components needed
// ✅ resetKeys for automatic recovery
// ✅ useErrorHandler hook (advanced use case)
// ✅ Better TypeScript support
// ✅ Smaller API surface
// ✅ Well-maintained library🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Level 1: Áp Dụng Concept (15 phút)
/**
* 🎯 Mục tiêu: Tạo Error Boundary cơ bản
* ⏱️ Thời gian: 15 phút
* 🚫 KHÔNG dùng: react-error-boundary library (tự implement)
*
* Requirements:
* 1. Tạo class-based Error Boundary
* 2. Show fallback UI khi có error
* 3. Log error ra console
* 4. Có button "Reset" để thử lại
*
* 💡 Gợi ý:
* - getDerivedStateFromError để update state
* - componentDidCatch để log
* - Reset bằng cách set hasError = false
*/
// Component that crashes after 3 seconds
function TimeBomb() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
if (seconds >= 3) {
throw new Error('💣 Time bomb exploded!');
}
return (
<div style={{ padding: 20, backgroundColor: '#fef3c7', borderRadius: 8 }}>
<h3>⏱️ Time Bomb</h3>
<p>Exploding in: {3 - seconds} seconds...</p>
</div>
);
}
// TODO: Implement SimpleErrorBoundary class component
// TODO: Use it to wrap TimeBomb
// Expected behavior:
// 1. Counter counts down 3, 2, 1
// 2. At 0 → Component throws error
// 3. Error Boundary catches → Shows fallback
// 4. Click Reset → Component re-mounts → Starts countdown again💡 Solution
/**
* Simple Error Boundary với Reset
*/
import { Component } from 'react';
class SimpleErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
};
}
static getDerivedStateFromError(error) {
// Update state to show fallback UI on next render
return {
hasError: true,
error,
};
}
componentDidCatch(error, errorInfo) {
// Log error details
console.error('🔴 Error caught by boundary:');
console.error('Error:', error.message);
console.error('Component stack:', errorInfo.componentStack);
}
handleReset = () => {
// Reset error state → Re-render children
this.setState({
hasError: false,
error: null,
});
};
render() {
if (this.state.hasError) {
return (
<div
style={{
padding: 24,
border: '3px solid #dc2626',
borderRadius: 12,
backgroundColor: '#fee',
textAlign: 'center',
}}
>
<div style={{ fontSize: 48, marginBottom: 12 }}>💥</div>
<h2 style={{ color: '#dc2626', marginBottom: 8 }}>
Component Crashed!
</h2>
<p
style={{
color: '#991b1b',
fontSize: 14,
marginBottom: 16,
fontFamily: 'monospace',
}}
>
{this.state.error?.message}
</p>
<button
onClick={this.handleReset}
style={{
padding: '12px 24px',
backgroundColor: '#dc2626',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
fontSize: 16,
fontWeight: 600,
}}
>
🔄 Reset & Try Again
</button>
</div>
);
}
return this.props.children;
}
}
function App() {
return (
<div style={{ padding: 20 }}>
<h1>Error Boundary Demo</h1>
<SimpleErrorBoundary>
<TimeBomb />
</SimpleErrorBoundary>
<div
style={{
marginTop: 24,
padding: 16,
backgroundColor: '#f0f0f0',
borderRadius: 8,
}}
>
<p style={{ margin: 0, fontSize: 14, color: '#666' }}>
ℹ️ This section is outside the error boundary. It will keep working
even when TimeBomb explodes!
</p>
</div>
</div>
);
}
// Kết quả:
// 0s: "Exploding in: 3 seconds..."
// 1s: "Exploding in: 2 seconds..."
// 2s: "Exploding in: 1 seconds..."
// 3s: 💥 Error UI appears với Reset button
// Click Reset → Countdown starts again⭐⭐ Level 2: Nhận Biết Pattern (25 phút)
/**
* 🎯 Mục tiêu: So sánh Error Boundary placement strategies
* ⏱️ Thời gian: 25 phút
*
* Scenario: Dashboard với 3 widgets
* - UserWidget: Critical (must show)
* - StatsWidget: Important
* - ChartWidget: Nice-to-have (often fails)
*
* 🤔 PHÂN TÍCH:
*
* Approach A: Single Error Boundary (bọc tất cả)
* Pros:
* - Code đơn giản nhất
* - Single fallback UI
*
* Cons:
* - Một widget fail → Toàn bộ dashboard unmounts
* - User không thấy gì cả
* - Bad UX
*
* Approach B: Individual Error Boundaries (mỗi widget riêng)
* Pros:
* - Isolated failures
* - Other widgets vẫn hoạt động
* - Best UX
*
* Cons:
* - More code
* - More boundaries to manage
*
* Approach C: Grouped Error Boundaries
* Pros:
* - Balance giữa isolation và simplicity
* - Critical widgets protected riêng
*
* Cons:
* - Phải quyết định nhóm nào
*
* 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
* Implement cả 3 approaches và compare UX
*/
// Mock widgets
function UserWidget() {
return (
<div style={{ padding: 16, backgroundColor: '#dbeafe', borderRadius: 8 }}>
<h3>👤 User Info</h3>
<p>John Doe - john@example.com</p>
</div>
);
}
function StatsWidget() {
return (
<div style={{ padding: 16, backgroundColor: '#d1fae5', borderRadius: 8 }}>
<h3>📊 Stats</h3>
<p>Total Users: 1,234 | Active: 567</p>
</div>
);
}
function ChartWidget() {
// 50% failure rate
if (Math.random() < 0.5) {
throw new Error('Chart data unavailable');
}
return (
<div style={{ padding: 16, backgroundColor: '#fef3c7', borderRadius: 8 }}>
<h3>📈 Chart</h3>
<p>Revenue trend: ↗️ +15% this month</p>
</div>
);
}
// TODO: Implement Approach A (Single boundary)
// TODO: Implement Approach B (Individual boundaries)
// TODO: Implement Approach C (Grouped boundaries)
// TODO: Add comparison notes💡 Solution
/**
* Error Boundary Placement Strategies
*/
import { ErrorBoundary } from 'react-error-boundary';
// Reusable fallback components
function WidgetErrorFallback({ error, resetErrorBoundary }) {
return (
<div
style={{
padding: 16,
backgroundColor: '#fee',
border: '2px dashed #dc2626',
borderRadius: 8,
textAlign: 'center',
}}
>
<div style={{ fontSize: 32, marginBottom: 8 }}>😞</div>
<p style={{ fontSize: 12, color: '#991b1b', marginBottom: 8 }}>
Widget failed to load
</p>
<button
onClick={resetErrorBoundary}
style={{
padding: '4px 12px',
fontSize: 12,
backgroundColor: '#dc2626',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
}}
>
Retry
</button>
</div>
);
}
function DashboardErrorFallback({ error, resetErrorBoundary }) {
return (
<div
style={{
padding: 32,
backgroundColor: '#fee',
border: '3px solid #dc2626',
borderRadius: 12,
textAlign: 'center',
}}
>
<div style={{ fontSize: 64, marginBottom: 16 }}>💥</div>
<h2 style={{ color: '#dc2626', marginBottom: 12 }}>
Dashboard Failed to Load
</h2>
<p style={{ color: '#991b1b', marginBottom: 16 }}>{error.message}</p>
<button
onClick={resetErrorBoundary}
style={{
padding: '12px 24px',
backgroundColor: '#dc2626',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
fontSize: 16,
}}
>
🔄 Reload Dashboard
</button>
</div>
);
}
// ============= APPROACH A: Single Boundary =============
function DashboardSingleBoundary() {
return (
<div style={{ padding: 20 }}>
<h2>❌ Approach A: Single Boundary</h2>
<p style={{ fontSize: 14, color: '#666', marginBottom: 16 }}>
Problem: If ChartWidget fails, entire dashboard disappears!
</p>
<ErrorBoundary FallbackComponent={DashboardErrorFallback}>
<div style={{ display: 'grid', gap: 16 }}>
<UserWidget />
<StatsWidget />
<ChartWidget />
</div>
</ErrorBoundary>
</div>
);
}
// ============= APPROACH B: Individual Boundaries =============
function DashboardIndividualBoundaries() {
return (
<div style={{ padding: 20 }}>
<h2>✅ Approach B: Individual Boundaries</h2>
<p style={{ fontSize: 14, color: '#666', marginBottom: 16 }}>
Best UX: Each widget isolated. Others continue working if one fails.
</p>
<div style={{ display: 'grid', gap: 16 }}>
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<UserWidget />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<StatsWidget />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<ChartWidget />
</ErrorBoundary>
</div>
</div>
);
}
// ============= APPROACH C: Grouped Boundaries =============
function DashboardGroupedBoundaries() {
return (
<div style={{ padding: 20 }}>
<h2>⚖️ Approach C: Grouped Boundaries</h2>
<p style={{ fontSize: 14, color: '#666', marginBottom: 16 }}>
Balanced: Critical widgets together, risky widget isolated.
</p>
<div style={{ display: 'grid', gap: 16 }}>
{/* Critical widgets: No error boundary (must show) */}
<div>
<UserWidget />
</div>
{/* Important but can fail as group */}
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<StatsWidget />
</ErrorBoundary>
{/* Risky widget: Isolated */}
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<ChartWidget />
</ErrorBoundary>
</div>
</div>
);
}
// ============= COMPARISON =============
function ComparisonApp() {
const [view, setView] = useState('individual');
return (
<div
style={{ padding: 20, backgroundColor: '#f9fafb', minHeight: '100vh' }}
>
<h1>Error Boundary Placement Strategies</h1>
{/* View Selector */}
<div style={{ marginBottom: 24 }}>
<button
onClick={() => setView('single')}
style={{
padding: '8px 16px',
marginRight: 8,
backgroundColor: view === 'single' ? '#3b82f6' : '#e5e7eb',
color: view === 'single' ? 'white' : '#374151',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
}}
>
Single Boundary
</button>
<button
onClick={() => setView('individual')}
style={{
padding: '8px 16px',
marginRight: 8,
backgroundColor: view === 'individual' ? '#3b82f6' : '#e5e7eb',
color: view === 'individual' ? 'white' : '#374151',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
}}
>
Individual Boundaries
</button>
<button
onClick={() => setView('grouped')}
style={{
padding: '8px 16px',
backgroundColor: view === 'grouped' ? '#3b82f6' : '#e5e7eb',
color: view === 'grouped' ? 'white' : '#374151',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
}}
>
Grouped Boundaries
</button>
</div>
{/* Render selected view */}
{view === 'single' && <DashboardSingleBoundary />}
{view === 'individual' && <DashboardIndividualBoundaries />}
{view === 'grouped' && <DashboardGroupedBoundaries />}
{/* Comparison Table */}
<div
style={{
marginTop: 40,
padding: 20,
backgroundColor: 'white',
borderRadius: 8,
}}
>
<h3>📊 Comparison Summary</h3>
<table
style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}
>
<thead>
<tr style={{ backgroundColor: '#f3f4f6' }}>
<th
style={{
padding: 12,
textAlign: 'left',
border: '1px solid #e5e7eb',
}}
>
Approach
</th>
<th
style={{
padding: 12,
textAlign: 'left',
border: '1px solid #e5e7eb',
}}
>
UX when ChartWidget fails
</th>
<th
style={{
padding: 12,
textAlign: 'left',
border: '1px solid #e5e7eb',
}}
>
Code Complexity
</th>
<th
style={{
padding: 12,
textAlign: 'left',
border: '1px solid #e5e7eb',
}}
>
Recommendation
</th>
</tr>
</thead>
<tbody>
<tr>
<td style={{ padding: 12, border: '1px solid #e5e7eb' }}>
<strong>Single</strong>
</td>
<td
style={{
padding: 12,
border: '1px solid #e5e7eb',
color: '#dc2626',
}}
>
❌ Entire dashboard disappears
</td>
<td
style={{
padding: 12,
border: '1px solid #e5e7eb',
color: '#059669',
}}
>
✅ Simplest
</td>
<td style={{ padding: 12, border: '1px solid #e5e7eb' }}>
❌ Avoid
</td>
</tr>
<tr style={{ backgroundColor: '#f9fafb' }}>
<td style={{ padding: 12, border: '1px solid #e5e7eb' }}>
<strong>Individual</strong>
</td>
<td
style={{
padding: 12,
border: '1px solid #e5e7eb',
color: '#059669',
}}
>
✅ Only ChartWidget shows error
</td>
<td
style={{
padding: 12,
border: '1px solid #e5e7eb',
color: '#d97706',
}}
>
⚠️ More verbose
</td>
<td
style={{
padding: 12,
border: '1px solid #e5e7eb',
color: '#059669',
}}
>
✅ Best UX
</td>
</tr>
<tr>
<td style={{ padding: 12, border: '1px solid #e5e7eb' }}>
<strong>Grouped</strong>
</td>
<td
style={{
padding: 12,
border: '1px solid #e5e7eb',
color: '#3b82f6',
}}
>
✅ ChartWidget shows error
</td>
<td
style={{
padding: 12,
border: '1px solid #e5e7eb',
color: '#3b82f6',
}}
>
⚖️ Balanced
</td>
<td
style={{
padding: 12,
border: '1px solid #e5e7eb',
color: '#3b82f6',
}}
>
⚖️ Good compromise
</td>
</tr>
</tbody>
</table>
</div>
{/* Decision Guide */}
<div
style={{
marginTop: 20,
padding: 20,
backgroundColor: '#eff6ff',
border: '1px solid #3b82f6',
borderRadius: 8,
}}
>
<h4 style={{ marginTop: 0, color: '#1e40af' }}>💡 Decision Guide</h4>
<ul style={{ fontSize: 14, lineHeight: 1.8 }}>
<li>
<strong>Individual Boundaries:</strong> When widgets are independent
and failures should be isolated
</li>
<li>
<strong>Grouped Boundaries:</strong> When some widgets logically
belong together
</li>
<li>
<strong>Single Boundary:</strong> Only for truly atomic features
that must work together
</li>
</ul>
<p style={{ fontSize: 14, margin: '16px 0 0', color: '#1e40af' }}>
<strong>Recommendation:</strong> Default to{' '}
<strong>Individual Boundaries</strong> for best UX. Only group when
there's a strong logical reason.
</p>
</div>
</div>
);
}
// 💭 QUYẾT ĐỊNH CỦA TÔI:
// Approach B (Individual Boundaries) là best choice vì:
// 1. Best user experience - Isolated failures
// 2. User vẫn thấy working widgets
// 3. Clear error messages per widget
// 4. Easy to retry individual widgets
// 5. Code clarity - Obvious what's protected
//
// Trade-off accepted:
// - Slightly more code (worth it for UX)
// - More boundaries to manage (still maintainable)⭐⭐⭐ Level 3: Kịch Bản Thực Tế (40 phút)
/**
* 🎯 Mục tiêu: E-commerce Product Page với comprehensive error handling
* ⏱️ Thời gian: 40 phút
*
* 📋 Product Requirements:
* User Story: "Là user, tôi muốn xem product details ngay cả khi
* một số sections fail, để tôi vẫn có thể mua hàng"
*
* ✅ Acceptance Criteria:
* - [ ] Product info PHẢI show (critical - có error boundary)
* - [ ] Reviews có thể fail gracefully (show fallback)
* - [ ] Recommendations có thể fail (hide section)
* - [ ] Add to cart PHẢI hoạt động dù reviews/recs fail
* - [ ] Each section có retry riêng
* - [ ] Error logging to console
*
* 🎨 Technical Constraints:
* - Product info: No error boundary (must show or page useless)
* - Reviews: Error boundary với retry
* - Recommendations: Error boundary, hide on error (graceful)
* - Use react-error-boundary library
*
* 🚨 Edge Cases cần handle:
* - Product API fails → Show top-level error
* - Reviews API fails → Show "Reviews unavailable"
* - Recommendations API fails → Hide section entirely
* - Multiple retries → Track attempt count
*
* 📝 Implementation Checklist:
* - [ ] Mock APIs với failure rates
* - [ ] 3 components (Product, Reviews, Recommendations)
* - [ ] Strategic error boundary placement
* - [ ] Custom fallback components
* - [ ] Retry with attempt tracking
* - [ ] Error logging
*/
// TODO: Implement ProductPage với comprehensive error handling💡 Solution
/**
* E-commerce Product Page - Production Error Handling
*/
import { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// ============= MOCK APIS =============
function fetchProduct(productId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 10% failure rate - Product is critical!
if (Math.random() < 0.1) {
reject(new Error('Failed to load product'));
} else {
resolve({
id: productId,
name: 'Premium Wireless Headphones',
price: 299,
description: 'High-quality sound with active noise cancellation',
inStock: true,
image: '🎧',
});
}
}, 500);
});
}
function fetchReviews(productId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 30% failure rate - Reviews can fail
if (Math.random() < 0.3) {
reject(new Error('Reviews service temporarily unavailable'));
} else {
resolve([
{
id: 1,
author: 'Alice',
rating: 5,
text: 'Excellent sound quality!',
},
{ id: 2, author: 'Bob', rating: 4, text: 'Great but a bit pricey' },
{
id: 3,
author: 'Charlie',
rating: 5,
text: 'Best headphones ever!',
},
]);
}
}, 800);
});
}
function fetchRecommendations(productId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 40% failure rate - Recommendations nice-to-have
if (Math.random() < 0.4) {
reject(new Error('Recommendations unavailable'));
} else {
resolve([
{ id: 101, name: 'Carrying Case', price: 29, image: '💼' },
{ id: 102, name: 'Audio Cable', price: 19, image: '🔌' },
{ id: 103, name: 'USB Charger', price: 25, image: '🔋' },
]);
}
}, 1000);
});
}
// ============= FALLBACK COMPONENTS =============
function ProductErrorFallback({ error, resetErrorBoundary }) {
return (
<div
style={{
padding: 40,
textAlign: 'center',
backgroundColor: '#fee',
border: '3px solid #dc2626',
borderRadius: 12,
}}
>
<div style={{ fontSize: 64, marginBottom: 16 }}>😞</div>
<h2 style={{ color: '#dc2626', marginBottom: 12 }}>
Product Unavailable
</h2>
<p style={{ color: '#991b1b', marginBottom: 16 }}>{error.message}</p>
<p style={{ fontSize: 14, color: '#7f1d1d', marginBottom: 20 }}>
This product cannot be displayed right now. Please try again.
</p>
<button
onClick={resetErrorBoundary}
style={{
padding: '12px 32px',
backgroundColor: '#dc2626',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
fontSize: 16,
fontWeight: 600,
}}
>
🔄 Try Again
</button>
</div>
);
}
function ReviewsErrorFallback({ error, resetErrorBoundary }) {
return (
<div
style={{
padding: 20,
backgroundColor: '#fef3c7',
border: '2px dashed #d97706',
borderRadius: 8,
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 12,
}}
>
<span style={{ fontSize: 32 }}>⚠️</span>
<div>
<h4 style={{ margin: 0, color: '#92400e' }}>
Reviews Temporarily Unavailable
</h4>
<p style={{ margin: '4px 0 0', fontSize: 13, color: '#78350f' }}>
{error.message}
</p>
</div>
</div>
<button
onClick={resetErrorBoundary}
style={{
padding: '8px 16px',
backgroundColor: '#d97706',
color: 'white',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
fontSize: 14,
}}
>
Retry Reviews
</button>
</div>
);
}
// ============= COMPONENTS =============
function ProductInfo({ productId }) {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchProduct(productId)
.then((data) => {
setProduct(data);
setLoading(false);
})
.catch((error) => {
// Let Error Boundary catch this
throw error;
});
}, [productId]);
if (loading) {
return (
<div style={{ padding: 40, textAlign: 'center' }}>
<div style={{ fontSize: 48, marginBottom: 16 }}>⏳</div>
<p>Loading product...</p>
</div>
);
}
return (
<div
style={{
padding: 24,
backgroundColor: 'white',
border: '2px solid #3b82f6',
borderRadius: 12,
}}
>
<div style={{ fontSize: 80, textAlign: 'center', marginBottom: 16 }}>
{product.image}
</div>
<h1 style={{ marginBottom: 8 }}>{product.name}</h1>
<p
style={{
fontSize: 32,
color: '#059669',
fontWeight: 'bold',
marginBottom: 16,
}}
>
${product.price}
</p>
<p style={{ color: '#6b7280', marginBottom: 16 }}>
{product.description}
</p>
<div style={{ marginBottom: 20 }}>
{product.inStock ? (
<span style={{ color: '#059669', fontWeight: 600 }}>✅ In Stock</span>
) : (
<span style={{ color: '#dc2626', fontWeight: 600 }}>
❌ Out of Stock
</span>
)}
</div>
<button
disabled={!product.inStock}
style={{
width: '100%',
padding: '16px',
backgroundColor: product.inStock ? '#3b82f6' : '#d1d5db',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: product.inStock ? 'pointer' : 'not-allowed',
fontSize: 18,
fontWeight: 600,
}}
>
{product.inStock ? '🛒 Add to Cart' : 'Out of Stock'}
</button>
</div>
);
}
function Reviews({ productId }) {
const [reviews, setReviews] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchReviews(productId)
.then((data) => {
setReviews(data);
setLoading(false);
})
.catch((error) => {
throw error; // Let Error Boundary catch
});
}, [productId]);
if (loading) {
return (
<div style={{ padding: 20, textAlign: 'center' }}>
<p>Loading reviews...</p>
</div>
);
}
return (
<div
style={{
padding: 20,
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: 8,
}}
>
<h3 style={{ marginBottom: 16 }}>
⭐ Customer Reviews ({reviews.length})
</h3>
{reviews.map((review) => (
<div
key={review.id}
style={{
marginBottom: 16,
padding: 16,
backgroundColor: '#f9fafb',
borderRadius: 8,
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 8,
}}
>
<strong>{review.author}</strong>
<span>{'⭐'.repeat(review.rating)}</span>
</div>
<p style={{ margin: 0, color: '#6b7280' }}>{review.text}</p>
</div>
))}
</div>
);
}
function Recommendations({ productId }) {
const [recommendations, setRecommendations] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchRecommendations(productId)
.then((data) => {
setRecommendations(data);
setLoading(false);
})
.catch((error) => {
throw error; // Let Error Boundary catch
});
}, [productId]);
if (loading) {
return (
<div style={{ padding: 20, textAlign: 'center' }}>
<p>Loading recommendations...</p>
</div>
);
}
return (
<div
style={{
padding: 20,
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: 8,
}}
>
<h3 style={{ marginBottom: 16 }}>🛍️ You Might Also Like</h3>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 12,
}}
>
{recommendations.map((item) => (
<div
key={item.id}
style={{
padding: 12,
border: '1px solid #e5e7eb',
borderRadius: 8,
textAlign: 'center',
}}
>
<div style={{ fontSize: 40, marginBottom: 8 }}>{item.image}</div>
<div style={{ fontSize: 14, marginBottom: 4 }}>{item.name}</div>
<div style={{ fontSize: 16, fontWeight: 'bold', color: '#059669' }}>
${item.price}
</div>
</div>
))}
</div>
</div>
);
}
// ============= MAIN PAGE =============
function ProductPage() {
const productId = '123';
const [reviewsKey, setReviewsKey] = useState(0);
const [recsKey, setRecsKey] = useState(0);
// Logging
const logError = (error, errorInfo) => {
console.error('🔴 Error logged:', {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
});
// TODO: Send to error tracking service (Sentry, etc.)
};
return (
<div
style={{
maxWidth: 800,
margin: '0 auto',
padding: 20,
backgroundColor: '#f9fafb',
minHeight: '100vh',
}}
>
<h1 style={{ marginBottom: 24 }}>Product Details</h1>
{/* CRITICAL: Product Info - Top-level error boundary */}
<div style={{ marginBottom: 24 }}>
<ErrorBoundary
FallbackComponent={ProductErrorFallback}
onError={logError}
>
<ProductInfo productId={productId} />
</ErrorBoundary>
</div>
{/* IMPORTANT: Reviews - Can fail with retry */}
<div style={{ marginBottom: 24 }}>
<ErrorBoundary
FallbackComponent={ReviewsErrorFallback}
onError={logError}
resetKeys={[reviewsKey]}
onReset={() => setReviewsKey((k) => k + 1)}
>
<Reviews productId={productId} />
</ErrorBoundary>
</div>
{/* NICE-TO-HAVE: Recommendations - Graceful degradation */}
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => {
// Silent failure - hide section entirely
console.warn(
'Recommendations failed (hiding section):',
error.message,
);
return null; // Don't show anything
}}
onError={(error) => {
console.warn('🟡 Recommendations unavailable:', error.message);
}}
resetKeys={[recsKey]}
onReset={() => setRecsKey((k) => k + 1)}
>
<Recommendations productId={productId} />
</ErrorBoundary>
{/* Debug info */}
<div
style={{
marginTop: 40,
padding: 16,
backgroundColor: '#eff6ff',
borderRadius: 8,
fontSize: 12,
fontFamily: 'monospace',
}}
>
<div style={{ fontWeight: 'bold', marginBottom: 8 }}>
🔍 Error Handling Strategy:
</div>
<div style={{ lineHeight: 1.8 }}>
<div>✅ Product: Top-level boundary - Must show or page useless</div>
<div>⚠️ Reviews: Medium-priority boundary - Show error + retry</div>
<div>💡 Recommendations: Low-priority - Hide on error (graceful)</div>
</div>
<div style={{ marginTop: 12, color: '#6b7280' }}>
Failure rates: Product 10% | Reviews 30% | Recommendations 40%
</div>
</div>
</div>
);
}
// Production Checklist:
// ✅ Strategic error boundary placement (3 levels)
// ✅ Custom fallback components per priority
// ✅ Retry mechanisms với reset keys
// ✅ Error logging (console + ready for service integration)
// ✅ Graceful degradation (recommendations hide on error)
// ✅ Product info protected but can show error
// ✅ Add to cart always accessible (unless product fails)
// ✅ Clear user communication about errors⭐⭐⭐⭐ Level 4: Quyết Định Kiến Trúc (60 phút)
/**
* 🎯 Mục tiêu: Thiết kế Error Boundary architecture cho SaaS Dashboard
* ⏱️ Thời gian: 60 phút
*
* 🏗️ PHASE 1: Research & Design (20 phút)
*
* Scenario: Multi-tenant SaaS Dashboard
* Components:
* - Navigation (critical - must always work)
* - User profile dropdown (critical)
* - Main workspace (critical - but widgets inside can fail)
* - 5+ widgets (analytics, notifications, activity, etc.)
* - Settings panel (important)
* - Help widget (nice-to-have)
*
* Nhiệm vụ:
* 1. So sánh ít nhất 3 error boundary strategies:
* A. Minimal (1-2 boundaries) - Simple but risky
* B. Comprehensive (boundary per component) - Safe but complex
* C. Strategic (boundaries at key isolation points) - Balanced
*
* 2. Document pros/cons:
* - User impact when errors occur
* - Developer experience
* - Maintenance burden
* - Performance implications
*
* 3. Error recovery strategies:
* - Auto-retry mechanisms
* - Fallback data/cache
* - User notification approaches
*
* 4. ADR (Architecture Decision Record):
* ```markdown
* # ADR: Error Boundary Strategy for SaaS Dashboard
*
* ## Context
* Multi-tenant dashboard với nhiều independent widgets.
* Users expect: High availability + Clear error communication.
* Business needs: Minimize support tickets từ errors.
*
* ## Decision
* [Strategy đã chọn]
*
* ## Rationale
* [Tại sao chọn strategy này]
* - User experience priorities: ...
* - Isolation requirements: ...
* - Recovery capabilities: ...
*
* ## Consequences
* Positive:
* - ...
*
* Negative:
* - ...
*
* Trade-offs accepted:
* - ...
*
* ## Implementation Guidelines
* 1. Boundary placement rules
* 2. Fallback UI standards
* 3. Error logging requirements
* 4. Recovery mechanisms
*
* ## Alternatives Considered
* 1. Strategy A: [Tại sao không chọn]
* 2. Strategy B: [Tại sao không chọn]
* ```
*
* 💻 PHASE 2: Implementation (30 phút)
* Implement chosen strategy với:
* - All major components
* - Proper error boundary placement
* - Custom fallbacks per priority level
* - Retry mechanisms
* - Error logging
*
* 🧪 PHASE 3: Testing Scenarios (10 phút)
* Test checklist:
* - [ ] Navigation error → App still usable?
* - [ ] Widget error → Other widgets work?
* - [ ] Multiple errors → UI graceful?
* - [ ] Retry → Works as expected?
* - [ ] Error logging → Captured properly?
*/
// TODO: Write comprehensive ADR
// TODO: Implement chosen error boundary architecture
// TODO: Create test scenarios💡 Solution
/**
* SaaS Dashboard - Strategic Error Boundary Architecture
*/
// ============= ADR =============
/*
# ADR: Error Boundary Strategy for SaaS Dashboard
## Context
Multi-tenant SaaS dashboard với:
- Critical navigation (header, sidebar) - Must always work
- Main workspace với 5+ independent widgets
- Settings panel - Important but can fail gracefully
- Help widget - Nice-to-have
Users expect:
- High availability (99%+ uptime feeling)
- Clear error communication
- Ability to continue working when non-critical features fail
Business needs:
- Minimize support tickets
- Maintain user trust
- Quick error recovery
## Decision
STRATEGIC ERROR BOUNDARY ARCHITECTURE with 4 levels:
Level 1: App-wide boundary (catastrophic errors only)
Level 2: Feature boundaries (navigation, workspace, settings)
Level 3: Widget boundaries (individual dashboard widgets)
Level 4: Component boundaries (complex components with known failure modes)
## Rationale
### User Experience:
1. Navigation failures → Show minimal navigation fallback (user can still access other pages)
2. Widget failures → Isolated to that widget only
3. Settings failures → Show error but keep dashboard working
4. Help widget → Silent failure (not critical)
### Isolation Requirements:
- Each widget must be isolated (failure doesn't cascade)
- Navigation protected separately (most critical)
- Settings isolated from main workspace
### Recovery Capabilities:
- Auto-retry for transient errors (1 attempt)
- Manual retry button for persistent errors
- Fallback to cached data where applicable
- Clear error messages with actionable steps
## Consequences
Positive:
+ Excellent user experience - isolated failures
+ Clear error boundaries for debugging
+ Flexible recovery strategies per component
+ Easy to add new widgets (pattern established)
+ Reduced support burden (users can self-recover)
Negative:
- More boundaries to maintain (4 levels)
- More boilerplate code
- Need consistent fallback UI design
- Error logging more complex
Trade-offs accepted:
- Code complexity for UX reliability
- More boundaries for better isolation
- Slightly larger bundle size for better error handling
## Implementation Guidelines
1. **Boundary Placement Rules:**
- App level: Catch unexpected crashes
- Feature level: Major sections (nav, workspace, settings)
- Widget level: Each dashboard widget
- Component level: Components with known failure modes
2. **Fallback UI Standards:**
- Critical (nav): Minimal but functional UI
- Important (widgets): Error message + retry
- Nice-to-have: Silent failure or hide
3. **Error Logging Requirements:**
- All errors logged to console (dev)
- Critical errors sent to monitoring service
- Include: timestamp, user ID, component path, error details
4. **Recovery Mechanisms:**
- Auto-retry once after 2s (transient errors)
- Manual retry button (persistent errors)
- Reset boundary on user action (when appropriate)
## Alternatives Considered
1. **Minimal Strategy (1-2 boundaries):**
❌ Rejected: Any error could unmount large sections
❌ Poor user experience
✅ Simplest code
2. **Comprehensive Strategy (boundary everywhere):**
❌ Rejected: Overkill, too much overhead
❌ Maintenance burden too high
✅ Maximum isolation
3. **Strategic Strategy (4 levels):** ✅ CHOSEN
✅ Balance UX + maintainability
✅ Clear isolation boundaries
⚖️ Moderate complexity
*/
// ============= IMPLEMENTATION =============
import { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// ============= LOGGING UTILITY =============
const errorLogger = {
log: (error, errorInfo, context) => {
const errorData = {
timestamp: new Date().toISOString(),
message: error.message,
stack: error.stack,
componentStack: errorInfo?.componentStack,
context,
// In real app: user ID, session ID, etc.
};
console.error('🔴 Error logged:', errorData);
// TODO: Send to monitoring service
// if (context.priority === 'critical') {
// sendToSentry(errorData);
// }
},
};
// ============= FALLBACK COMPONENTS =============
// Level 1: App-wide catastrophic error
function AppErrorFallback({ error, resetErrorBoundary }) {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
backgroundColor: '#fee',
padding: 20,
}}
>
<div
style={{
maxWidth: 500,
padding: 40,
backgroundColor: 'white',
border: '3px solid #dc2626',
borderRadius: 12,
textAlign: 'center',
}}
>
<div style={{ fontSize: 64, marginBottom: 16 }}>💥</div>
<h1 style={{ color: '#dc2626', marginBottom: 12 }}>
Application Error
</h1>
<p style={{ color: '#991b1b', marginBottom: 20 }}>
Something went wrong. We've been notified and are working on it.
</p>
<button
onClick={resetErrorBoundary}
style={{
padding: '12px 24px',
backgroundColor: '#dc2626',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
fontSize: 16,
fontWeight: 600,
}}
>
Reload Application
</button>
</div>
</div>
);
}
// Level 2: Navigation error
function NavigationErrorFallback() {
return (
<div
style={{
padding: 16,
backgroundColor: '#fef3c7',
borderBottom: '2px solid #d97706',
}}
>
<p style={{ margin: 0, color: '#92400e', fontSize: 14 }}>
⚠️ Navigation temporarily unavailable. Dashboard still accessible below.
</p>
</div>
);
}
// Level 3: Widget error
function WidgetErrorFallback({ error, resetErrorBoundary, widgetName }) {
return (
<div
style={{
padding: 20,
backgroundColor: '#fee',
border: '2px dashed #dc2626',
borderRadius: 8,
textAlign: 'center',
}}
>
<div style={{ fontSize: 32, marginBottom: 8 }}>😞</div>
<h4 style={{ margin: '0 0 8px', color: '#dc2626' }}>
{widgetName} Unavailable
</h4>
<p style={{ fontSize: 13, color: '#991b1b', marginBottom: 12 }}>
{error.message}
</p>
<button
onClick={resetErrorBoundary}
style={{
padding: '6px 16px',
backgroundColor: '#dc2626',
color: 'white',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
fontSize: 13,
}}
>
Retry
</button>
</div>
);
}
// ============= MOCK COMPONENTS =============
function Navigation() {
// 5% failure rate
if (Math.random() < 0.05) {
throw new Error('Navigation service unavailable');
}
return (
<nav
style={{
padding: 16,
backgroundColor: '#1e40af',
color: 'white',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div style={{ fontWeight: 'bold', fontSize: 18 }}>SaaS Dashboard</div>
<div style={{ display: 'flex', gap: 16 }}>
<a
href='#'
style={{ color: 'white' }}
>
Dashboard
</a>
<a
href='#'
style={{ color: 'white' }}
>
Analytics
</a>
<a
href='#'
style={{ color: 'white' }}
>
Settings
</a>
</div>
<div>👤 User</div>
</nav>
);
}
function Widget({ name, failureRate = 0.2 }) {
if (Math.random() < failureRate) {
throw new Error(`${name} data fetch failed`);
}
return (
<div
style={{
padding: 20,
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: 8,
}}
>
<h3 style={{ marginTop: 0 }}>{name}</h3>
<div style={{ color: '#6b7280' }}>
Lorem ipsum data visualization here...
</div>
<div
style={{
marginTop: 12,
fontSize: 24,
fontWeight: 'bold',
color: '#3b82f6',
}}
>
{Math.floor(Math.random() * 1000)}
</div>
</div>
);
}
// ============= DASHBOARD LAYOUT =============
function Dashboard() {
const [navKey, setNavKey] = useState(0);
const [analyticsKey, setAnalyticsKey] = useState(0);
const [activityKey, setActivityKey] = useState(0);
const [notificationsKey, setNotificationsKey] = useState(0);
const [statsKey, setStatsKey] = useState(0);
return (
<div style={{ backgroundColor: '#f9fafb', minHeight: '100vh' }}>
{/* LEVEL 1: App-wide boundary */}
<ErrorBoundary
FallbackComponent={AppErrorFallback}
onError={(error, errorInfo) => {
errorLogger.log(error, errorInfo, {
level: 'app',
priority: 'critical',
});
}}
>
{/* LEVEL 2: Navigation boundary */}
<ErrorBoundary
FallbackComponent={NavigationErrorFallback}
resetKeys={[navKey]}
onReset={() => setNavKey((k) => k + 1)}
onError={(error, errorInfo) => {
errorLogger.log(error, errorInfo, {
level: 'feature',
component: 'navigation',
priority: 'critical',
});
}}
>
<Navigation />
</ErrorBoundary>
{/* LEVEL 2: Workspace boundary */}
<div style={{ padding: 20 }}>
<h1 style={{ marginBottom: 24 }}>Dashboard Overview</h1>
{/* LEVEL 3: Widget boundaries */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: 16,
}}
>
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<WidgetErrorFallback
error={error}
resetErrorBoundary={resetErrorBoundary}
widgetName='Analytics'
/>
)}
resetKeys={[analyticsKey]}
onReset={() => setAnalyticsKey((k) => k + 1)}
onError={(error, errorInfo) => {
errorLogger.log(error, errorInfo, {
level: 'widget',
widget: 'analytics',
priority: 'high',
});
}}
>
<Widget
name='📊 Analytics'
failureRate={0.3}
/>
</ErrorBoundary>
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<WidgetErrorFallback
error={error}
resetErrorBoundary={resetErrorBoundary}
widgetName='Activity'
/>
)}
resetKeys={[activityKey]}
onReset={() => setActivityKey((k) => k + 1)}
onError={(error, errorInfo) => {
errorLogger.log(error, errorInfo, {
level: 'widget',
widget: 'activity',
priority: 'medium',
});
}}
>
<Widget
name='🎯 Activity Feed'
failureRate={0.25}
/>
</ErrorBoundary>
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<WidgetErrorFallback
error={error}
resetErrorBoundary={resetErrorBoundary}
widgetName='Notifications'
/>
)}
resetKeys={[notificationsKey]}
onReset={() => setNotificationsKey((k) => k + 1)}
onError={(error, errorInfo) => {
errorLogger.log(error, errorInfo, {
level: 'widget',
widget: 'notifications',
priority: 'medium',
});
}}
>
<Widget
name='🔔 Notifications'
failureRate={0.2}
/>
</ErrorBoundary>
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<WidgetErrorFallback
error={error}
resetErrorBoundary={resetErrorBoundary}
widgetName='Stats'
/>
)}
resetKeys={[statsKey]}
onReset={() => setStatsKey((k) => k + 1)}
onError={(error, errorInfo) => {
errorLogger.log(error, errorInfo, {
level: 'widget',
widget: 'stats',
priority: 'low',
});
}}
>
<Widget
name='📈 Stats'
failureRate={0.15}
/>
</ErrorBoundary>
{/* Help widget - Graceful degradation (no fallback UI) */}
<ErrorBoundary
fallbackRender={() => null}
onError={(error) => {
console.warn('Help widget failed (hidden):', error.message);
}}
>
<Widget
name='❓ Help'
failureRate={0.4}
/>
</ErrorBoundary>
</div>
</div>
{/* Architecture Documentation */}
<div
style={{
margin: 20,
padding: 20,
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: 8,
fontSize: 13,
fontFamily: 'monospace',
}}
>
<div style={{ fontWeight: 'bold', marginBottom: 12 }}>
🏗️ Error Boundary Architecture:
</div>
<div style={{ lineHeight: 1.8 }}>
<div>Level 1 (App): Catastrophic errors → Full page error</div>
<div>Level 2 (Navigation): 5% failure → Minimal fallback</div>
<div>Level 3 (Widgets):</div>
<div style={{ paddingLeft: 20 }}>
- Analytics: 30% failure → Error + retry
</div>
<div style={{ paddingLeft: 20 }}>
- Activity: 25% failure → Error + retry
</div>
<div style={{ paddingLeft: 20 }}>
- Notifications: 20% failure → Error + retry
</div>
<div style={{ paddingLeft: 20 }}>
- Stats: 15% failure → Error + retry
</div>
<div style={{ paddingLeft: 20 }}>
- Help: 40% failure → Silent (hidden)
</div>
</div>
<div style={{ marginTop: 12, color: '#6b7280' }}>
💡 Each widget isolated - failures don't cascade
</div>
</div>
</ErrorBoundary>
</div>
);
}
// Test Checklist Results:
// ✅ Navigation error → Minimal nav shown, dashboard still usable
// ✅ Widget error → Only that widget shows error, others work fine
// ✅ Multiple errors → Each widget independent, UI remains stable
// ✅ Retry → Reset key mechanism works, widget re-mounts
// ✅ Error logging → All errors captured with context
// ✅ Help widget → Silent failure, doesn't distract user
// ✅ Critical path protected → App never fully crashes⭐⭐⭐⭐⭐ Level 5: Production Challenge (90 phút)
/**
* 🎯 Mục tiêu: Complete Error Handling System
* ⏱️ Thời gian: 90 phút
*
* 📋 Feature Specification:
*
* Tạo comprehensive error handling system cho real-world app với:
* 1. Error boundary hierarchy (4+ levels)
* 2. Error recovery strategies (retry, fallback data, cache)
* 3. Error logging & monitoring integration
* 4. User notification system
* 5. Error analytics dashboard
*
* Features:
* - Automatic retry với exponential backoff
* - Fallback to cached data khi API fails
* - Error toast notifications
* - Error dashboard showing all errors
* - Context-aware error messages
* - Recovery suggestions
*
* 🏗️ Technical Design Doc:
*
* 1. Error Boundary Hierarchy:
* - Level 1: App root (catch-all)
* - Level 2: Route/page level
* - Level 3: Feature/section level
* - Level 4: Component level
*
* 2. Error Recovery System:
* - Auto-retry service với backoff
* - Cache layer cho fallback data
* - Manual retry với UI feedback
* - Reset mechanisms
*
* 3. Error Logging Architecture:
* - Local logging (console)
* - Remote logging (mock service)
* - Error aggregation
* - Priority-based routing
*
* 4. User Notification Strategy:
* - Toast notifications (non-blocking)
* - Inline error messages (blocking)
* - Error dashboard (detailed view)
* - Recovery suggestions
*
* 5. Error Analytics:
* - Error count by type
* - Error rate trends
* - Component failure rates
* - User impact metrics
*
* ✅ Production Checklist:
* - [ ] Error boundary hierarchy implemented
* - [ ] Auto-retry với backoff
* - [ ] Fallback data system
* - [ ] Error logging service
* - [ ] Toast notification system
* - [ ] Error dashboard
* - [ ] Recovery mechanisms
* - [ ] User-friendly error messages
* - [ ] Error analytics tracking
* - [ ] Documentation complete
*
* 📝 Documentation:
* - Error boundary placement diagram
* - Recovery flow charts
* - Logging schema
* - User notification rules
*
* 🔍 Code Review Self-Checklist:
* - [ ] All critical paths protected
* - [ ] Error messages user-friendly
* - [ ] Recovery options clear
* - [ ] Logging comprehensive
* - [ ] Performance impact minimal
* - [ ] Code well-documented
*/
// TODO: Implement complete error handling system
// Gợi ý: Bắt đầu với error logging service, sau đó boundaries, cuối cùng analytics💡 Solution
/**
* Production-Grade Error Handling System
*
* Features:
* - 4-level error boundary hierarchy
* - Automatic retry với exponential backoff
* - Fallback data caching
* - Comprehensive error logging
* - Toast notifications
* - Error analytics dashboard
*/
import { useState, useEffect, createContext, useContext } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// ============= ERROR LOGGING SERVICE =============
class ErrorLoggingService {
constructor() {
this.errors = [];
this.listeners = [];
}
log(error, errorInfo, context) {
const errorEntry = {
id: Date.now() + Math.random(),
timestamp: new Date().toISOString(),
message: error.message,
stack: error.stack,
componentStack: errorInfo?.componentStack,
context,
resolved: false,
};
this.errors.push(errorEntry);
this.notifyListeners();
// Log to console
console.error('🔴 Error:', errorEntry);
// TODO: Send to remote service
// this.sendToRemote(errorEntry);
return errorEntry.id;
}
getErrors() {
return [...this.errors];
}
getErrorStats() {
const total = this.errors.length;
const byLevel = this.errors.reduce((acc, err) => {
const level = err.context?.level || 'unknown';
acc[level] = (acc[level] || 0) + 1;
return acc;
}, {});
const byPriority = this.errors.reduce((acc, err) => {
const priority = err.context?.priority || 'unknown';
acc[priority] = (acc[priority] || 0) + 1;
return acc;
}, {});
return { total, byLevel, byPriority };
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
}
notifyListeners() {
this.listeners.forEach((listener) => listener(this.errors));
}
clearErrors() {
this.errors = [];
this.notifyListeners();
}
}
const errorLogger = new ErrorLoggingService();
// ============= RETRY SERVICE =============
class RetryService {
async retryWithBackoff(fn, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, attempt - 1) * 1000;
console.log(`Retry attempt ${attempt} failed, waiting ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
}
const retryService = new RetryService();
// ============= CACHE SERVICE =============
class CacheService {
constructor() {
this.cache = new Map();
}
set(key, value) {
this.cache.set(key, {
value,
timestamp: Date.now(),
});
}
get(key, maxAge = 60000) {
// 1 minute default
const entry = this.cache.get(key);
if (!entry) return null;
const age = Date.now() - entry.timestamp;
if (age > maxAge) {
this.cache.delete(key);
return null;
}
return entry.value;
}
clear() {
this.cache.clear();
}
}
const cacheService = new CacheService();
// ============= NOTIFICATION CONTEXT =============
const NotificationContext = createContext();
function NotificationProvider({ children }) {
const [notifications, setNotifications] = useState([]);
const addNotification = (message, type = 'error') => {
const id = Date.now();
setNotifications((prev) => [...prev, { id, message, type }]);
// Auto-remove after 5s
setTimeout(() => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
}, 5000);
};
const removeNotification = (id) => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
};
return (
<NotificationContext.Provider
value={{ addNotification, removeNotification }}
>
{children}
<ToastContainer
notifications={notifications}
onRemove={removeNotification}
/>
</NotificationContext.Provider>
);
}
function useNotifications() {
return useContext(NotificationContext);
}
// ============= TOAST COMPONENT =============
function ToastContainer({ notifications, onRemove }) {
return (
<div
style={{
position: 'fixed',
top: 20,
right: 20,
zIndex: 9999,
display: 'flex',
flexDirection: 'column',
gap: 12,
}}
>
{notifications.map((notification) => (
<div
key={notification.id}
style={{
padding: 16,
backgroundColor: notification.type === 'error' ? '#fee' : '#d1fae5',
border: `2px solid ${notification.type === 'error' ? '#dc2626' : '#059669'}`,
borderRadius: 8,
maxWidth: 300,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'start',
}}
>
<div
style={{
color: notification.type === 'error' ? '#991b1b' : '#065f46',
fontSize: 14,
}}
>
{notification.message}
</div>
<button
onClick={() => onRemove(notification.id)}
style={{
marginLeft: 12,
padding: 0,
backgroundColor: 'transparent',
border: 'none',
cursor: 'pointer',
fontSize: 18,
color: notification.type === 'error' ? '#991b1b' : '#065f46',
}}
>
×
</button>
</div>
))}
</div>
);
}
// ============= ERROR ANALYTICS DASHBOARD =============
function ErrorAnalyticsDashboard() {
const [errors, setErrors] = useState([]);
const [stats, setStats] = useState(null);
useEffect(() => {
const updateStats = () => {
setErrors(errorLogger.getErrors());
setStats(errorLogger.getErrorStats());
};
updateStats();
const unsubscribe = errorLogger.subscribe(updateStats);
return unsubscribe;
}, []);
if (!stats || errors.length === 0) {
return (
<div
style={{
padding: 40,
textAlign: 'center',
backgroundColor: '#f0fdf4',
border: '1px solid #86efac',
borderRadius: 8,
}}
>
<div style={{ fontSize: 48, marginBottom: 12 }}>✅</div>
<h3 style={{ color: '#065f46' }}>No Errors!</h3>
<p style={{ color: '#166534' }}>Your app is running smoothly.</p>
</div>
);
}
return (
<div
style={{
padding: 20,
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: 8,
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
}}
>
<h2 style={{ margin: 0 }}>📊 Error Analytics</h2>
<button
onClick={() => errorLogger.clearErrors()}
style={{
padding: '8px 16px',
backgroundColor: '#dc2626',
color: 'white',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
}}
>
Clear All
</button>
</div>
{/* Stats Grid */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 16,
marginBottom: 20,
}}
>
<div
style={{
padding: 16,
backgroundColor: '#fee',
borderRadius: 8,
textAlign: 'center',
}}
>
<div style={{ fontSize: 32, fontWeight: 'bold', color: '#dc2626' }}>
{stats.total}
</div>
<div style={{ fontSize: 14, color: '#991b1b' }}>Total Errors</div>
</div>
<div
style={{
padding: 16,
backgroundColor: '#fef3c7',
borderRadius: 8,
textAlign: 'center',
}}
>
<div style={{ fontSize: 32, fontWeight: 'bold', color: '#d97706' }}>
{stats.byPriority.critical || 0}
</div>
<div style={{ fontSize: 14, color: '#92400e' }}>Critical</div>
</div>
<div
style={{
padding: 16,
backgroundColor: '#dbeafe',
borderRadius: 8,
textAlign: 'center',
}}
>
<div style={{ fontSize: 32, fontWeight: 'bold', color: '#3b82f6' }}>
{Object.keys(stats.byLevel).length}
</div>
<div style={{ fontSize: 14, color: '#1e40af' }}>Affected Levels</div>
</div>
</div>
{/* Error List */}
<div>
<h3>Recent Errors:</h3>
<div style={{ maxHeight: 300, overflowY: 'auto' }}>
{errors
.slice()
.reverse()
.map((err, idx) => (
<div
key={err.id}
style={{
padding: 12,
marginBottom: 8,
backgroundColor: '#f9fafb',
border: '1px solid #e5e7eb',
borderRadius: 6,
fontSize: 13,
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 4,
}}
>
<strong style={{ color: '#dc2626' }}>
{err.context?.component || 'Unknown Component'}
</strong>
<span style={{ fontSize: 11, color: '#6b7280' }}>
{new Date(err.timestamp).toLocaleTimeString()}
</span>
</div>
<div style={{ color: '#991b1b', marginBottom: 4 }}>
{err.message}
</div>
<div style={{ display: 'flex', gap: 8, fontSize: 11 }}>
<span
style={{
padding: '2px 6px',
backgroundColor:
err.context?.priority === 'critical'
? '#fecaca'
: '#fed7aa',
borderRadius: 4,
color: '#7f1d1d',
}}
>
{err.context?.priority || 'unknown'}
</span>
<span
style={{
padding: '2px 6px',
backgroundColor: '#dbeafe',
borderRadius: 4,
color: '#1e40af',
}}
>
{err.context?.level || 'unknown'}
</span>
</div>
</div>
))}
</div>
</div>
</div>
);
}
// ============= PRODUCTION READY APP =============
function ProductionApp() {
return (
<NotificationProvider>
<div
style={{ padding: 20, backgroundColor: '#f9fafb', minHeight: '100vh' }}
>
<h1>Production Error Handling System</h1>
<Dashboard />
<div style={{ marginTop: 40 }}>
<ErrorAnalyticsDashboard />
</div>
<div
style={{
marginTop: 40,
padding: 20,
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: 8,
}}
>
<h3>📚 System Documentation</h3>
<div style={{ fontSize: 14, lineHeight: 1.8 }}>
<p>
<strong>Error Boundary Hierarchy:</strong>
</p>
<ul>
<li>Level 1: App-wide catastrophic errors</li>
<li>Level 2: Feature-level boundaries (navigation, workspace)</li>
<li>Level 3: Widget-level isolation</li>
<li>Level 4: Component-level (where needed)</li>
</ul>
<p>
<strong>Recovery Mechanisms:</strong>
</p>
<ul>
<li>Auto-retry with exponential backoff (1s, 2s, 4s)</li>
<li>Cache fallback for failed API calls</li>
<li>Manual retry buttons</li>
<li>Reset keys for component remounting</li>
</ul>
<p>
<strong>Notification Strategy:</strong>
</p>
<ul>
<li>Toast notifications for non-critical errors</li>
<li>Inline error messages for critical failures</li>
<li>Error analytics dashboard for monitoring</li>
</ul>
</div>
</div>
</div>
</NotificationProvider>
);
}
// Production Checklist:
// ✅ Error boundary hierarchy (4 levels) implemented
// ✅ Auto-retry với exponential backoff
// ✅ Fallback data caching system
// ✅ Comprehensive error logging service
// ✅ Toast notification system
// ✅ Error analytics dashboard
// ✅ Recovery mechanisms (retry, reset, cache)
// ✅ User-friendly error messages
// ✅ Error tracking and analytics
// ✅ Complete documentation
// Kết quả:
// 1. Multiple error boundary levels protect different parts
// 2. Errors are logged và tracked trong analytics dashboard
// 3. Users receive toast notifications
// 4. Each component có retry mechanism
// 5. Graceful degradation - app continues working
// 6. Error statistics visible in dashboard📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh Trade-offs
| Pattern | Pros | Cons | When to use |
|---|---|---|---|
| Class-based Error Boundary | - Full control - No dependencies - React native solution | - More boilerplate - Class component syntax - Harder to compose | ❌ Avoid (use library instead) |
| react-error-boundary Library | - Function component API - Reset keys - Less boilerplate - Well-maintained | - External dependency - Learning curve | ✅ Production apps ✅ All new projects |
| Single Top-level Boundary | - Simple setup - One place to manage | - Bad UX - No isolation | ❌ Only for POCs |
| Individual Boundaries | - Best isolation - Best UX - Independent failures | - More code - More boundaries | ✅ Production apps ✅ Independent features |
| Strategic Multi-level | - Balance complexity/UX - Clear hierarchy - Flexible | - Requires planning - Moderate complexity | ✅ Large applications ✅ Complex UIs |
Decision Tree
START: Need Error Boundary?
|
├─ Yes, for production app
│ |
│ ├─ Use react-error-boundary library
│ |
│ ├─ Multiple independent features?
│ │ |
│ │ ├─ Yes → Individual boundaries per feature
│ │ └─ No → Single boundary OK
│ |
│ └─ Complex dashboard/workspace?
│ |
│ └─ Yes → Multi-level strategic boundaries
│
└─ No, just learning
|
└─ Implement class-based for understanding🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Error Boundary Không Catch Event Handler Error
/**
* 🐛 BUG: Error boundary không catch error từ button click
*/
function BuggyButton() {
const handleClick = () => {
throw new Error('Button error!');
};
return <button onClick={handleClick}>Click me</button>;
}
function App() {
return (
<ErrorBoundary>
<BuggyButton />
</ErrorBoundary>
);
}
// ❓ TẠI SAO: Error boundary không catch?
// ❓ LÀM SAO FIX?💡 Giải thích & Fix
/**
* GIẢI THÍCH:
* Error Boundary KHÔNG catch errors trong:
* 1. Event handlers
* 2. Async code (setTimeout, Promises)
* 3. Server-side rendering
* 4. Errors trong chính Error Boundary
*
* LÝ DO: Event handlers chạy NGOÀI React render cycle
* Error Boundary chỉ catch errors trong render phase
*/
// ✅ FIX: Dùng try-catch trong event handler
function FixedButton() {
const [error, setError] = useState(null);
const handleClick = () => {
try {
throw new Error('Button error!');
} catch (err) {
setError(err);
console.error('Error in event handler:', err);
// Show toast notification
alert('An error occurred: ' + err.message);
}
};
if (error) {
return (
<div style={{ padding: 16, backgroundColor: '#fee', borderRadius: 8 }}>
<p style={{ color: '#dc2626' }}>Error: {error.message}</p>
<button onClick={() => setError(null)}>Clear Error</button>
</div>
);
}
return <button onClick={handleClick}>Click me</button>;
}
// 💡 PATTERN: Error state trong component cho event handler errorsBug 2: Error Boundary Loop - Infinite Re-render
/**
* 🐛 BUG: Error boundary keeps re-rendering infinitely
*/
function AlwaysFails() {
throw new Error('I always fail!');
}
function App() {
const [resetKey, setResetKey] = useState(0);
return (
<div>
<button onClick={() => setResetKey((k) => k + 1)}>
Reset (Key: {resetKey})
</button>
<ErrorBoundary
FallbackComponent={ErrorFallback}
resetKeys={[resetKey]}
>
<AlwaysFails />
</ErrorBoundary>
</div>
);
}
// ❓ VẤN ĐỀ: Component always fails → User clicks reset → Fails again → Frustrating!
// ❓ GIẢI PHÁP?💡 Giải thích & Fix
/**
* GIẢI THÍCH:
* Component luôn throw error → Reset chỉ trigger re-mount → Fail lại
* Cần limit retry attempts và show permanent error state
*/
// ✅ FIX 1: Track retry attempts
function SmartErrorBoundary({ children, maxRetries = 3 }) {
const [retryCount, setRetryCount] = useState(0);
const [resetKey, setResetKey] = useState(0);
const handleReset = () => {
if (retryCount < maxRetries) {
setRetryCount((c) => c + 1);
setResetKey((k) => k + 1);
}
};
const handleError = (error, errorInfo) => {
console.error(`Error (attempt ${retryCount + 1}/${maxRetries}):`, error);
};
const FallbackWithRetries = ({ error, resetErrorBoundary }) => {
const retriesLeft = maxRetries - retryCount;
return (
<div style={{ padding: 20, backgroundColor: '#fee', borderRadius: 8 }}>
<h3 style={{ color: '#dc2626' }}>Error Occurred</h3>
<p>{error.message}</p>
{retriesLeft > 0 ? (
<button
onClick={() => {
resetErrorBoundary();
handleReset();
}}
>
🔄 Try Again ({retriesLeft} attempts left)
</button>
) : (
<div>
<p style={{ color: '#991b1b', fontWeight: 'bold' }}>
❌ Maximum retry attempts reached
</p>
<p style={{ fontSize: 14 }}>
Please refresh the page or contact support.
</p>
</div>
)}
</div>
);
};
return (
<ErrorBoundary
FallbackComponent={FallbackWithRetries}
resetKeys={[resetKey]}
onError={handleError}
>
{children}
</ErrorBoundary>
);
}
// ✅ FIX 2: Automatic backoff delay
function BackoffErrorBoundary({ children }) {
const [retryCount, setRetryCount] = useState(0);
const [canRetry, setCanRetry] = useState(true);
const handleReset = () => {
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s, 8s...
setCanRetry(false);
setTimeout(() => {
setRetryCount((c) => c + 1);
setCanRetry(true);
}, delay);
};
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button
onClick={() => {
resetErrorBoundary();
handleReset();
}}
disabled={!canRetry}
>
{canRetry ? `Retry (attempt ${retryCount + 1})` : 'Waiting...'}
</button>
</div>
)}
>
{children}
</ErrorBoundary>
);
}
// 💡 BEST PRACTICE: Limit retries + exponential backoffBug 3: Missing Reset - Component Doesn't Re-mount
/**
* 🐛 BUG: Retry button doesn't actually retry
*/
class BrokenErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
handleRetry = () => {
// ❌ BUG: Chỉ set hasError = false không re-mount children!
this.setState({ hasError: false });
};
render() {
if (this.state.hasError) {
return (
<div>
<p>Something went wrong</p>
<button onClick={this.handleRetry}>Retry</button>
</div>
);
}
return this.props.children;
}
}
// ❓ VẤN ĐỀ: Component state vẫn giữ error state cũ
// ❓ FIX?💡 Giải thích & Fix
/**
* GIẢI THÍCH:
* Reset error boundary state KHÔNG tự động reset children state
* Cần force re-mount bằng key prop
*/
// ✅ FIX 1: Use key prop to force re-mount
function App() {
const [resetKey, setResetKey] = useState(0);
return (
<ErrorBoundary
fallbackRender={({ resetErrorBoundary }) => (
<div>
<p>Error occurred</p>
<button
onClick={() => {
setResetKey((k) => k + 1); // Change key
resetErrorBoundary(); // Reset boundary
}}
>
Retry
</button>
</div>
)}
>
<BuggyComponent key={resetKey} /> {/* Key forces re-mount */}
</ErrorBoundary>
);
}
// ✅ FIX 2: Use resetKeys prop (react-error-boundary)
function App() {
const [resetKey, setResetKey] = useState(0);
return (
<ErrorBoundary
resetKeys={[resetKey]} // Auto reset when key changes
fallbackRender={({ resetErrorBoundary }) => (
<button onClick={() => setResetKey((k) => k + 1)}>Retry</button>
)}
>
<BuggyComponent />
</ErrorBoundary>
);
}
// ✅ FIX 3: Reset internal state trong children
function BuggyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// Reset state when component re-mounts
setCount(0);
}, []);
if (count === 3) {
throw new Error('Count reached 3!');
}
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</div>
);
}
// 💡 KEY INSIGHT: Error boundary reset ≠ Component re-mount
// Use keys or resetKeys to force fresh component instance✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu Error Boundary là gì và tại sao cần dùng
- [ ] Tôi biết Error Boundary KHÔNG catch errors trong event handlers
- [ ] Tôi biết cách implement class-based Error Boundary
- [ ] Tôi biết khi nào dùng react-error-boundary library
- [ ] Tôi hiểu strategic placement của error boundaries
- [ ] Tôi biết cách implement retry mechanisms
- [ ] Tôi hiểu graceful degradation strategies
- [ ] Tôi biết cách combine Error Boundaries với Suspense
- [ ] Tôi biết cách log errors cho production monitoring
- [ ] Tôi hiểu trade-offs của different boundary strategies
Code Review Checklist
Error Boundary Implementation:
- [ ] Dùng react-error-boundary library (không tự implement)
- [ ] Có custom fallback components phù hợp
- [ ] Error logging được implement đúng
- [ ] Retry mechanisms có limit attempts
- [ ] Reset keys được sử dụng đúng
Boundary Placement:
- [ ] Critical paths được protect
- [ ] Independent features có boundaries riêng
- [ ] Không có single top-level boundary cho toàn app
- [ ] Graceful degradation cho nice-to-have features
User Experience:
- [ ] Error messages user-friendly
- [ ] Có retry options khi appropriate
- [ ] Loading/error states rõ ràng
- [ ] App vẫn usable khi có errors
Production Readiness:
- [ ] Error tracking service integration ready
- [ ] Error analytics implemented
- [ ] Documentation đầy đủ
- [ ] Testing coverage cho error scenarios
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
Implement Error Boundary cho form với validation:
- Form có 3 fields: name, email, password
- Email field có validation phức tạp (có thể throw error)
- Error boundary bọc field, không ảnh hưởng toàn form
- Retry mechanism cho failed validation
- Error logging
Nâng cao (60 phút)
Tạo Error Recovery System:
- Multiple components với different failure rates
- Automatic retry với exponential backoff
- Cache fallback data
- Error analytics dashboard
- Toast notifications
- Complete error handling documentation
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
Đọc thêm
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền
- Ngày 49: Suspense for Data Fetching - Error Boundaries bọc Suspense
- Ngày 24: Custom Hooks - Error handling hooks
- Ngày 16-20: useEffect - Async error handling
Hướng tới
- Ngày 51: React Server Components - Error boundaries trong RSC
- Ngày 53-57: Testing - Testing error boundaries
- Ngày 58-59: TypeScript - Typing error boundaries
💡 SENIOR INSIGHTS
Cân Nhắc Production
1. Error Tracking Services:
// Sentry integration
componentDidCatch(error, errorInfo) {
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack
}
}
});
}2. User Context:
// Include user info in error logs
const logError = (error, errorInfo) => {
const userId = getCurrentUser()?.id;
const sessionId = getSessionId();
errorLogger.log(error, errorInfo, {
userId,
sessionId,
userAgent: navigator.userAgent,
timestamp: Date.now(),
});
};3. Feature Flags:
// Disable features with high error rates
const FeatureWithKillSwitch = () => {
const featureEnabled = useFeatureFlag('new-dashboard');
if (!featureEnabled) {
return <LegacyDashboard />;
}
return (
<ErrorBoundary
onError={(error) => {
// If errors exceed threshold, disable feature
if (getErrorRate('new-dashboard') > 0.05) {
disableFeature('new-dashboard');
}
}}
>
<NewDashboard />
</ErrorBoundary>
);
};Câu Hỏi Phỏng Vấn
Junior:
- Error Boundary là gì?
- Error Boundary catch những errors nào?
- Làm sao implement Error Boundary?
Mid:
- Tại sao Error Boundary không catch event handler errors?
- So sánh getDerivedStateFromError vs componentDidCatch
- Strategic placement của Error Boundaries như thế nào?
Senior:
- Thiết kế error handling architecture cho large-scale app
- Error recovery strategies trong production
- Monitoring và alerting cho errors trong React app
War Stories
Story 1: The Dashboard That Never Died "Ở startup cũ, dashboard có 20+ widgets. Ban đầu dùng single error boundary - một widget fail thì toàn bộ dashboard crash. Users phàn nàn nhiều.
Refactor sang individual boundaries cho mỗi widget. Error rate giảm 80% vì:
- Users vẫn thấy working widgets
- Có retry button rõ ràng
- Error messages specific hơn
Lesson: Isolation > Simplicity trong production apps."
Story 2: The Infinite Retry Loop "Launch feature mới, có bug khiến component always fail. Users spam retry button → Server overload → Cascade failure.
Fix bằng cách:
- Limit retry attempts (max 3)
- Exponential backoff (1s, 2s, 4s)
- Rate limiting ở client-side
Lesson: Always limit retry mechanisms."
🎯 PREVIEW NGÀY 51
Ngày mai chúng ta sẽ học về React Server Components (RSC):
- RSC concept và architecture
- Server vs Client components
- Benefits và trade-offs
- When to use RSC
- Preview cho Next.js module
Error Boundaries sẽ được dùng kết hợp với RSC để handle errors trong server components!
🎉 CHÚC MỪNG! Bạn đã hoàn thành Ngày 50!
Bạn đã học được: ✅ Error Boundary concept và implementation ✅ react-error-boundary library ✅ Strategic boundary placement ✅ Error recovery strategies ✅ Production error handling patterns
Hôm nay là ngày cuối của Modern React Features phase. Bạn đã nắm vững React 18 concurrent features và error handling. Chuẩn bị cho phase tiếp theo về Testing & Quality! 🚀