π‘οΈ Task 10.3: Error Handling Hardening - Bulletproof Demo
Implement comprehensive error handling and graceful degradation for a bulletproof demo.
π Description
Add defensive error handling throughout the application to ensure nothing crashes during the demo. Implement error boundaries, API error handling, WebSocket reconnection logic, fallback states, user-friendly error messages, and automatic recovery mechanisms. The goal is that even if something goes wrong, the demo continues gracefully.
π― Acceptance Criteria
π οΈ Implementation
Error Boundary Component
Create src/components/ErrorBoundary.js:
import React from 'react';
import './ErrorBoundary.css';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error Boundary caught error:', error, errorInfo);
this.setState({
error,
errorInfo,
});
// Log to error reporting service (if you have one)
// logErrorToService(error, errorInfo);
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
window.location.reload();
};
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<div className="error-content">
<div className="error-icon">β οΈ</div>
<h1>Something Went Wrong</h1>
<p className="error-message">
The application encountered an unexpected error.
This shouldn't happen during the demo!
</p>
{process.env.NODE_ENV === 'development' && this.state.error && (
<details className="error-details">
<summary>Error Details (Development Only)</summary>
<pre className="error-stack">
{this.state.error.toString()}
{this.state.errorInfo?.componentStack}
</pre>
</details>
)}
<div className="error-actions">
<button onClick={this.handleReset} className="primary-button">
Reload Application
</button>
<button
onClick={() => window.location.href = '/'}
className="secondary-button"
>
Go to Home
</button>
</div>
<p className="error-help">
If this persists, please contact the development team.
</p>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Create src/components/ErrorBoundary.css:
.error-boundary {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
background: var(--dark-bg);
}
.error-content {
max-width: 600px;
text-align: center;
}
.error-icon {
font-size: 4rem;
margin-bottom: 1rem;
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
.error-content h1 {
margin: 1rem 0;
color: var(--danger-red);
}
.error-message {
margin: 1rem 0 2rem;
color: var(--text-secondary);
line-height: 1.6;
}
.error-details {
margin: 2rem 0;
text-align: left;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
padding: 1rem;
}
.error-details summary {
cursor: pointer;
font-weight: 600;
color: var(--text-primary);
}
.error-stack {
margin-top: 1rem;
padding: 1rem;
background: #000;
border-radius: 4px;
color: #ff6b6b;
font-size: 0.75rem;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.error-actions {
display: flex;
gap: 1rem;
justify-content: center;
margin: 2rem 0;
}
.primary-button, .secondary-button {
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.primary-button {
background: var(--info-blue);
color: white;
border: none;
}
.primary-button:hover {
background: #1976D2;
transform: translateY(-2px);
}
.secondary-button {
background: transparent;
color: var(--text-primary);
border: 1px solid var(--border);
}
.secondary-button:hover {
background: rgba(255, 255, 255, 0.05);
}
.error-help {
margin-top: 2rem;
font-size: 0.875rem;
color: var(--text-muted);
}
Wrap App with Error Boundary in src/index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import ErrorBoundary from './components/ErrorBoundary';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>
);
API Error Handling
Update src/services/api.js:
import axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';
// Create axios instance with defaults
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 30000, // 30 second timeout
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor
api.interceptors.request.use(
config => {
console.log(`[API] ${config.method.toUpperCase()} ${config.url}`);
return config;
},
error => {
console.error('[API] Request error:', error);
return Promise.reject(error);
}
);
// Response interceptor
api.interceptors.response.use(
response => {
console.log(`[API] Response from ${response.config.url}:`, response.status);
return response;
},
error => {
// Handle different error types
if (error.response) {
// Server responded with error status
console.error(`[API] Server error ${error.response.status}:`, error.response.data);
const errorMessage = error.response.data?.error ||
error.response.data?.message ||
`Server error: ${error.response.status}`;
throw new Error(errorMessage);
} else if (error.request) {
// Request made but no response
console.error('[API] No response from server:', error.request);
throw new Error('Unable to connect to server. Please check your connection.');
} else {
// Error setting up request
console.error('[API] Request setup error:', error.message);
throw new Error(`Request error: ${error.message}`);
}
}
);
// Retry logic for failed requests
async function apiWithRetry(fn, maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
const isLastAttempt = i === maxRetries - 1;
if (isLastAttempt) {
throw error;
}
console.log(`[API] Retry attempt ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
}
}
}
export const disasterAPI = {
trigger: async (disasterData) => {
return apiWithRetry(async () => {
const response = await api.post('/disaster/trigger', disasterData);
return response.data;
});
},
getDisaster: async (disasterId) => {
return apiWithRetry(async () => {
const response = await api.get(`/disaster/${disasterId}`);
return response.data;
});
},
getPlan: async (disasterId) => {
return apiWithRetry(async () => {
const response = await api.get(`/disaster/${disasterId}/plan`);
return response.data;
});
},
healthCheck: async () => {
try {
const response = await api.get('/health', { timeout: 5000 });
return response.data;
} catch (error) {
return { status: 'error', message: error.message };
}
},
};
export default api;
WebSocket Error Handling
Update src/services/websocket.js:
import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
import io from 'socket.io-client';
const WebSocketContext = createContext(null);
export function useWebSocketContext() {
const context = useContext(WebSocketContext);
if (!context) {
throw new Error('useWebSocketContext must be used within WebSocketProvider');
}
return context;
}
export function WebSocketProvider({ children, url }) {
const [socket, setSocket] = useState(null);
const [connected, setConnected] = useState(false);
const [lastUpdate, setLastUpdate] = useState(null);
const [error, setError] = useState(null);
const reconnectAttempts = useRef(0);
const reconnectTimer = useRef(null);
const maxReconnectAttempts = 10;
const reconnectDelay = 3000;
useEffect(() => {
console.log('[WebSocket] Initializing connection...');
const socketUrl = url || process.env.REACT_APP_API_URL?.replace('/api', '') || 'http://localhost:5000';
function connect() {
const newSocket = io(socketUrl, {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: maxReconnectAttempts,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 10000,
});
// Connection events
newSocket.on('connect', () => {
console.log('[WebSocket] Connected successfully');
setConnected(true);
setError(null);
reconnectAttempts.current = 0;
});
newSocket.on('disconnect', (reason) => {
console.log('[WebSocket] Disconnected:', reason);
setConnected(false);
if (reason === 'io server disconnect') {
// Server initiated disconnect, try manual reconnect
handleReconnect(newSocket);
}
});
newSocket.on('connect_error', (err) => {
console.error('[WebSocket] Connection error:', err.message);
setError(err.message);
reconnectAttempts.current += 1;
if (reconnectAttempts.current >= maxReconnectAttempts) {
console.error('[WebSocket] Max reconnection attempts reached');
setError('Unable to establish connection. Using fallback mode.');
} else {
handleReconnect(newSocket);
}
});
newSocket.on('reconnect', (attemptNumber) => {
console.log(`[WebSocket] Reconnected after ${attemptNumber} attempts`);
setConnected(true);
setError(null);
});
newSocket.on('reconnect_failed', () => {
console.error('[WebSocket] Reconnection failed');
setError('Connection failed. System will operate in offline mode.');
});
newSocket.on('error', (err) => {
console.error('[WebSocket] Socket error:', err);
setError('WebSocket error occurred');
});
setSocket(newSocket);
}
function handleReconnect(socket) {
if (reconnectTimer.current) {
clearTimeout(reconnectTimer.current);
}
reconnectTimer.current = setTimeout(() => {
console.log('[WebSocket] Attempting manual reconnect...');
socket.connect();
}, reconnectDelay);
}
connect();
return () => {
console.log('[WebSocket] Cleaning up connection');
if (reconnectTimer.current) {
clearTimeout(reconnectTimer.current);
}
if (socket) {
socket.disconnect();
socket.removeAllListeners();
}
};
}, [url]);
const contextValue = {
socket,
connected,
lastUpdate,
error,
isReady: socket && connected && !error,
};
return (
<WebSocketContext.Provider value={contextValue}>
{children}
</WebSocketContext.Provider>
);
}
export default WebSocketContext;
Component-Level Error Handling
Update src/hooks/useDisaster.js with error recovery:
import { useState, useCallback, useEffect } from 'react';
import { disasterAPI } from '../services/api';
import useWebSocket from './useWebSocket';
function useDisaster() {
const [disaster, setDisaster] = useState(null);
const [plan, setPlan] = useState(null);
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(0);
const [error, setError] = useState(null);
const [statusMessage, setStatusMessage] = useState('');
const [retryCount, setRetryCount] = useState(0);
const { connected, on, error: wsError } = useWebSocket();
const triggerDisaster = useCallback(async (disasterData) => {
try {
console.log('[useDisaster] Triggering disaster:', disasterData);
setLoading(true);
setProgress(0);
setError(null);
setPlan(null);
setRetryCount(0);
setStatusMessage('Initializing disaster simulation...');
const response = await disasterAPI.trigger(disasterData);
console.log('[useDisaster] Disaster triggered successfully:', response);
setDisaster(response);
if (response.disaster_id && connected) {
// Subscribe to WebSocket updates
subscribeToDisaster(response.disaster_id);
setStatusMessage('Subscribed to real-time updates');
} else if (!connected) {
setStatusMessage('WebSocket unavailable - using fallback mode');
// Could implement polling fallback here
}
} catch (err) {
console.error('[useDisaster] Failed to trigger disaster:', err);
const errorMessage = err.message || 'Failed to trigger simulation';
setError(errorMessage);
setLoading(false);
setStatusMessage('Error occurred');
// Offer retry
if (retryCount < 2) {
setStatusMessage(`Error: ${errorMessage}. Retry available.`);
}
}
}, [connected, retryCount]);
const retryTrigger = useCallback(() => {
setRetryCount(prev => prev + 1);
// Would need to store last disasterData to retry
}, []);
const clearDisaster = useCallback(() => {
console.log('[useDisaster] Clearing disaster state');
setDisaster(null);
setPlan(null);
setLoading(false);
setProgress(0);
setError(null);
setStatusMessage('');
setRetryCount(0);
}, []);
// Handle WebSocket errors
useEffect(() => {
if (wsError && loading) {
console.warn('[useDisaster] WebSocket error during processing:', wsError);
setStatusMessage('Connection issue - switching to fallback mode...');
// Could implement fallback to polling here
}
}, [wsError, loading]);
// Timeout protection
useEffect(() => {
if (loading) {
const timeout = setTimeout(() => {
if (progress < 100) {
console.error('[useDisaster] Processing timeout');
setError('Processing timeout - system may be overloaded');
setLoading(false);
}
}, 120000); // 2 minute timeout
return () => clearTimeout(timeout);
}
}, [loading, progress]);
return {
disaster,
plan,
loading,
progress,
error,
statusMessage,
triggerDisaster,
clearDisaster,
retryTrigger,
isProcessing: loading,
hasActivePlan: !!plan,
hasError: !!error,
canRetry: retryCount < 2 && error,
};
}
export default useDisaster;
Invalid Data Handling
Add validation utility src/utils/validation.js:
export function validateDisaster(disaster) {
if (!disaster) return false;
if (!disaster.disaster_id) return false;
if (!disaster.location || !disaster.location.lat || !disaster.location.lon) return false;
return true;
}
export function validatePlan(plan) {
if (!plan) return false;
if (!plan.disaster_id) return false;
if (!plan.executive_summary) return false;
return true;
}
export function sanitizeString(str) {
if (typeof str !== 'string') return '';
return str.replace(/<script[^>]*>.*?<\/script>/gi, '')
.replace(/<[^>]+>/g, '')
.trim();
}
export function sanitizeNumber(num, fallback = 0) {
const parsed = Number(num);
return isNaN(parsed) ? fallback : parsed;
}
Use validation in components:
import { validatePlan } from '../utils/validation';
function PlanViewer({ plan }) {
if (!validatePlan(plan)) {
return (
<div className="plan-viewer error-state">
<h2>Invalid Plan Data</h2>
<p>The system generated invalid data. Please try again.</p>
</div>
);
}
// ... render plan
}
π§ͺ Error Testing Scenarios
Test each scenario to ensure graceful handling:
SCENARIO 1: Backend Offline
1. Stop backend server
2. Try to trigger disaster
3. β Error message displays
4. β App doesn't crash
5. β Can dismiss error
6. β Retry option available
SCENARIO 2: WebSocket Disconnect
1. Start disaster processing
2. Disable network mid-process
3. β "Connection lost" message
4. β Progress bar pauses gracefully
5. β Re-enable network
6. β Reconnects automatically
SCENARIO 3: Invalid JSON Response
1. Mock API to return malformed JSON
2. Trigger disaster
3. β Error caught and displayed
4. β No console errors crash app
5. β Can recover
SCENARIO 4: Timeout
1. Mock slow API (130+ seconds)
2. Trigger disaster
3. β Timeout error after 2 minutes
4. β Clear error message
5. β Can retry
SCENARIO 5: Missing Map Token
1. Remove REACT_APP_MAPBOX_TOKEN
2. Load app
3. β Map shows error state
4. β Rest of app works
5. β Plan still displays
SCENARIO 6: Component Error
1. Force a React component error
2. β Error boundary catches it
3. β Shows friendly error page
4. β Can reload app
5. β Other features unaffected
π Error Handling Checklist
β±οΈ Estimated Time
90 minutes
π Related Documentation
React Error Boundaries: https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
π‘οΈ Task 10.3: Error Handling Hardening - Bulletproof Demo
Implement comprehensive error handling and graceful degradation for a bulletproof demo.
π Description
Add defensive error handling throughout the application to ensure nothing crashes during the demo. Implement error boundaries, API error handling, WebSocket reconnection logic, fallback states, user-friendly error messages, and automatic recovery mechanisms. The goal is that even if something goes wrong, the demo continues gracefully.
π― Acceptance Criteria
π οΈ Implementation
Error Boundary Component
Create
src/components/ErrorBoundary.js:Create
src/components/ErrorBoundary.css:Wrap App with Error Boundary in
src/index.js:API Error Handling
Update
src/services/api.js:WebSocket Error Handling
Update
src/services/websocket.js:Component-Level Error Handling
Update
src/hooks/useDisaster.jswith error recovery:Invalid Data Handling
Add validation utility
src/utils/validation.js:Use validation in components:
π§ͺ Error Testing Scenarios
Test each scenario to ensure graceful handling:
π Error Handling Checklist
β±οΈ Estimated Time
90 minutes
π Related Documentation
React Error Boundaries: https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary