On a space station, the failure of one system cannot mean the end of the mission. Every critical system has backups, and the crew is trained to work in emergency mode. Your React application should work the same way - even if something goes wrong, the user should be able to continue working.
Graceful degradation is a design philosophy in which the application continues to function even when some of its parts fail - simply with a reduced level of functionality.
1// Instead of: entire page crashes
2function BrokenApp() {
3 return <WhiteScreenOfDeath />; // User can't do anything
4}
5
6// Graceful degradation: parts of the app still work
7function ResilientApp() {
8 return (
9 <div>
10 <Navigation /> {/* Always works */}
11 <ErrorBoundary fallback={<FallbackContent />}>
12 <MainContent /> {/* Can break */}
13 </ErrorBoundary>
14 <Footer /> {/* Always works */}
15 </div>
16 );
17}When an operation fails, give the user the option to try again:
1import { ErrorBoundary } from 'react-error-boundary';
2
3function ErrorFallbackWithRetry({ error, resetErrorBoundary }) {
4 return (
5 <div className="error-panel">
6 <h3>Module failure</h3>
7 <p>Error: {error.message}</p>
8 <button onClick={resetErrorBoundary}>
9 Retry
10 </button>
11 </div>
12 );
13}
14
15function App() {
16 return (
17 <ErrorBoundary
18 FallbackComponent={ErrorFallbackWithRetry}
19 onReset={() => {
20 // Clear cache, reset state, etc.
21 }}
22 >
23 <DataPanel />
24 </ErrorBoundary>
25 );
26}Instead of showing an error, show alternative content:
1function WeatherWidget() {
2 const [data, setData] = useState(null);
3 const [error, setError] = useState(false);
4
5 useEffect(() => {
6 fetchWeather()
7 .then(setData)
8 .catch(() => setError(true));
9 }, []);
10
11 if (error) {
12 // Instead of an error, show static data
13 return (
14 <div className="weather-widget offline">
15 <p>Weather data unavailable</p>
16 <p>Last update: 2h ago</p>
17 <small>Working on restoring the connection</small>
18 </div>
19 );
20 }
21
22 if (!data) return <WeatherSkeleton />;
23 return <WeatherDisplay data={data} />;
24}Handle partial failures - when some data cannot be fetched:
1function MissionDashboard() {
2 const [missions, setMissions] = useState([]);
3 const [stats, setStats] = useState(null);
4 const [statsError, setStatsError] = useState(false);
5
6 useEffect(() => {
7 // Fetch data in parallel
8 Promise.allSettled([
9 fetch('/api/missions').then(r => r.json()),
10 fetch('/api/stats').then(r => r.json())
11 ]).then(([missionsResult, statsResult]) => {
12 if (missionsResult.status === 'fulfilled') {
13 setMissions(missionsResult.value);
14 }
15 if (statsResult.status === 'fulfilled') {
16 setStats(statsResult.value);
17 } else {
18 setStatsError(true);
19 }
20 });
21 }, []);
22
23 return (
24 <div>
25 <h2>Mission Panel</h2>
26
27 {/* Stats - may be unavailable */}
28 {statsError ? (
29 <p className="warning">Statistics temporarily unavailable</p>
30 ) : stats ? (
31 <StatsPanel data={stats} />
32 ) : (
33 <StatsSkeleton />
34 )}
35
36 {/* Mission list - always try to display */}
37 <MissionList missions={missions} />
38 </div>
39 );
40}For critical data, implement automatic retries with increasing delays:
1async function fetchWithRetry(url, maxRetries = 3) {
2 for (let attempt = 0; attempt < maxRetries; attempt++) {
3 try {
4 const response = await fetch(url);
5 if (!response.ok) throw new Error('Request failed');
6 return await response.json();
7 } catch (error) {
8 if (attempt === maxRetries - 1) throw error;
9 // Exponential backoff: 1s, 2s, 4s
10 const delay = Math.pow(2, attempt) * 1000;
11 await new Promise(resolve => setTimeout(resolve, delay));
12 }
13 }
14}
15
16function CriticalData() {
17 const [data, setData] = useState(null);
18 const [error, setError] = useState(null);
19 const [retrying, setRetrying] = useState(false);
20
21 const loadData = async () => {
22 setRetrying(true);
23 setError(null);
24 try {
25 const result = await fetchWithRetry('/api/critical-data');
26 setData(result);
27 } catch (err) {
28 setError(err);
29 } finally {
30 setRetrying(false);
31 }
32 };
33
34 useEffect(() => { loadData(); }, []);
35
36 if (error) return (
37 <div>
38 <p>Failed to fetch data after 3 attempts</p>
39 <button onClick={loadData}>Try again</button>
40 </div>
41 );
42 if (retrying) return <p>Retrying connection...</p>;
43 if (!data) return <LoadingSkeleton />;
44 return <DataDisplay data={data} />;
45}The best practice is to combine both mechanisms:
1<ErrorBoundary FallbackComponent={ErrorUI}>
2 <Suspense fallback={<LoadingSkeleton />}>
3 <LazyComponent />
4 </Suspense>
5</ErrorBoundary>Error Boundary catches errors, and Suspense handles loading. Together they create a complete safety system.