PatternsAdvanced Error Handling
Graceful Degradation
Maintain functionality when features fail with smart fallback strategies
Graceful degradation ensures your app remains functional even when parts fail. Instead of showing error screens, degrade gracefully by providing fallback experiences.
Core Concept
Goal: Keep the app usable, even with reduced functionality.
// ❌ BAD: All or nothing
function Dashboard() {
const analytics = fetchAnalytics(); // Throws on failure
const notifications = fetchNotifications(); // Throws on failure
return (
<>
<Analytics data={analytics} />
<Notifications data={notifications} />
</>
);
}
// If analytics fails → entire dashboard crashes
// ✅ GOOD: Graceful degradation
function Dashboard() {
const { data: analytics, error: analyticsError } = useAnalytics();
const { data: notifications, error: notificationsError } = useNotifications();
return (
<>
{analyticsError ? (
<AnalyticsPlaceholder message="Analytics temporarily unavailable" />
) : (
<Analytics data={analytics} />
)}
{notificationsError ? (
<NotificationsPlaceholder />
) : (
<Notifications data={notifications} />
)}
</>
);
}
// Analytics fails → show placeholder, rest of dashboard worksFallback Strategies
1. Cached Data Fallback
// hooks/useDataWithCache.ts
import { useQuery } from '@tanstack/react-query';
export function useDataWithCache<T>(key: string, fetcher: () => Promise<T>) {
return useQuery({
queryKey: [key],
queryFn: fetcher,
// Use stale data while refetching
staleTime: 5 * 60 * 1000, // 5 min
// Keep data in cache
cacheTime: 30 * 60 * 1000, // 30 min
// Show stale data on error
placeholderData: (previousData) => previousData,
// Retry logic
retry: 2,
});
}
// Usage
function ProductList() {
const { data, error, isStale } = useDataWithCache('products', fetchProducts);
return (
<div>
{isStale && (
<Banner type="warning">
Showing cached data. Unable to refresh.
</Banner>
)}
<Products data={data} />
</div>
);
}2. Default/Static Fallback
// Provide sensible defaults when data unavailable
function UserProfile() {
const { data: user, error } = useUser();
// Use default profile when fetch fails
const profile = user ?? {
name: 'Guest User',
avatar: '/default-avatar.png',
preferences: getDefaultPreferences(),
};
return (
<>
{error && (
<Notice>Using default profile. Unable to load your data.</Notice>
)}
<Profile user={profile} />
</>
);
}3. Feature Detection Fallback
// components/VideoPlayer.tsx
'use client';
import { useState, useEffect } from 'react';
export function VideoPlayer({ src }: { src: string }) {
const [supportsVideo, setSupportsVideo] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
// Check if browser supports video
const video = document.createElement('video');
setSupportsVideo(video.canPlayType('video/mp4') !== '');
}, []);
if (!supportsVideo) {
return (
<div className="video-fallback">
<p>Video not supported in your browser.</p>
<a href={src} download>Download video</a>
</div>
);
}
if (error) {
return (
<div className="video-fallback">
<p>Unable to load video.</p>
<a href={src}>View video directly</a>
</div>
);
}
return (
<video
src={src}
controls
onError={() => setError(true)}
/>
);
}4. Progressive Enhancement
// Start with basic functionality, enhance when possible
function SearchBox() {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState<string[]>([]);
const [autocompleteAvailable, setAutocompleteAvailable] = useState(true);
const fetchSuggestions = async (q: string) => {
try {
const results = await fetch(`/api/autocomplete?q=${q}`).then(r => r.json());
setSuggestions(results);
} catch (error) {
// Autocomplete failed, disable feature
setAutocompleteAvailable(false);
setSuggestions([]);
}
};
return (
<div>
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
if (autocompleteAvailable) {
fetchSuggestions(e.target.value);
}
}}
placeholder="Search..."
/>
{/* Search works with or without autocomplete */}
{autocompleteAvailable && suggestions.length > 0 && (
<SuggestionsList suggestions={suggestions} />
)}
<button onClick={() => performSearch(query)}>
Search
</button>
</div>
);
}Service-Level Degradation
External Service Fallback
// lib/image-service.ts
export async function getOptimizedImage(url: string) {
try {
// Try CDN first
return await fetch(`https://cdn.example.com/optimize?url=${url}`)
.then(r => r.json());
} catch (error) {
console.warn('CDN unavailable, using original image');
// Fallback to original image
return {
url: url,
optimized: false,
};
}
}
// Usage
function ProductImage({ src }: { src: string }) {
const { data } = useQuery(['image', src], () => getOptimizedImage(src));
return (
<div>
<img src={data?.url || src} alt="Product" />
{!data?.optimized && (
<small>High-quality image optimization unavailable</small>
)}
</div>
);
}Analytics Degradation
// lib/analytics.ts
class Analytics {
private available = true;
track(event: string, properties?: Record<string, any>) {
if (!this.available) {
// Analytics failed, log locally for debugging
console.log('[Analytics (degraded)]', event, properties);
return;
}
try {
// Try to send to analytics service
if (typeof gtag !== 'undefined') {
gtag('event', event, properties);
} else {
throw new Error('gtag not available');
}
} catch (error) {
console.warn('Analytics unavailable, degrading gracefully');
this.available = false;
// Don't break the app, just lose analytics
console.log('[Analytics (degraded)]', event, properties);
}
}
}
export const analytics = new Analytics();
// Usage - App continues even if analytics fails
function ProductPage() {
useEffect(() => {
analytics.track('page_view', { page: 'product' });
}, []);
return <Product />;
}UI Degradation Patterns
Skeleton → Error → Retry
// components/DegradableContent.tsx
'use client';
import { useState } from 'react';
interface Props {
fetchData: () => Promise<any>;
renderData: (data: any) => React.ReactNode;
renderSkeleton: () => React.ReactNode;
renderFallback?: (error: Error) => React.ReactNode;
}
export function DegradableContent({
fetchData,
renderData,
renderSkeleton,
renderFallback,
}: Props) {
const [state, setState] = useState<{
status: 'loading' | 'success' | 'error' | 'degraded';
data?: any;
error?: Error;
}>({ status: 'loading' });
useEffect(() => {
fetchData()
.then(data => setState({ status: 'success', data }))
.catch(error => setState({ status: 'error', error }));
}, []);
if (state.status === 'loading') {
return <>{renderSkeleton()}</>;
}
if (state.status === 'success') {
return <>{renderData(state.data)}</>;
}
if (state.status === 'error') {
if (renderFallback) {
return <>{renderFallback(state.error!)}</>;
}
return (
<div className="degraded-content">
<p>Unable to load content</p>
<button onClick={() => setState({ status: 'loading' })}>
Try Again
</button>
</div>
);
}
return null;
}Progressive Image Loading
// components/ProgressiveImage.tsx
'use client';
import { useState } from 'react';
import Image from 'next/image';
interface Props {
src: string;
lowQualitySrc?: string;
alt: string;
}
export function ProgressiveImage({ src, lowQualitySrc, alt }: Props) {
const [imageError, setImageError] = useState(false);
const [highQualityLoaded, setHighQualityLoaded] = useState(false);
if (imageError) {
// Ultimate fallback: colored placeholder
return (
<div className="image-placeholder">
<span>{alt}</span>
</div>
);
}
return (
<div className="progressive-image">
{/* Low quality placeholder (loads fast) */}
{lowQualitySrc && !highQualityLoaded && (
<Image
src={lowQualitySrc}
alt={alt}
className="blur"
fill
/>
)}
{/* High quality image */}
<Image
src={src}
alt={alt}
fill
onLoad={() => setHighQualityLoaded(true)}
onError={() => setImageError(true)}
/>
</div>
);
}API Response Degradation
Partial Data Handling
// Handle partial API failures
interface DashboardData {
user: UserData;
stats?: Stats; // Optional
notifications?: Notification[]; // Optional
activity?: Activity[]; // Optional
}
async function fetchDashboard(): Promise<DashboardData> {
const [user, stats, notifications, activity] = await Promise.allSettled([
fetchUser(),
fetchStats(),
fetchNotifications(),
fetchActivity(),
]);
// User data is required
if (user.status === 'rejected') {
throw new Error('Failed to load user data');
}
// Other data is optional - use undefined if failed
return {
user: user.value,
stats: stats.status === 'fulfilled' ? stats.value : undefined,
notifications: notifications.status === 'fulfilled' ? notifications.value : undefined,
activity: activity.status === 'fulfilled' ? activity.value : undefined,
};
}
// Component handles missing data gracefully
function Dashboard() {
const { data } = useQuery('dashboard', fetchDashboard);
return (
<div>
<UserProfile user={data.user} />
{data.stats ? (
<Stats data={data.stats} />
) : (
<StatsPlaceholder message="Stats unavailable" />
)}
{data.notifications ? (
<Notifications data={data.notifications} />
) : (
<NotificationsPlaceholder />
)}
{data.activity ? (
<Activity data={data.activity} />
) : (
<ActivityPlaceholder />
)}
</div>
);
}Timeout with Fallback
// lib/fetch-with-timeout.ts
export async function fetchWithTimeout<T>(
url: string,
options: RequestInit = {},
timeout = 5000
): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// Usage with fallback
async function getRecommendations() {
try {
return await fetchWithTimeout('/api/recommendations', {}, 3000);
} catch (error) {
console.warn('Recommendations timeout, using defaults');
// Fallback to static recommendations
return getDefaultRecommendations();
}
}Network-Aware Degradation
// hooks/useNetworkAwareDegradation.ts
import { useState, useEffect } from 'react';
export function useNetworkAwareDegradation() {
const [quality, setQuality] = useState<'high' | 'low' | 'offline'>('high');
useEffect(() => {
const updateQuality = () => {
if (!navigator.onLine) {
setQuality('offline');
return;
}
const connection = (navigator as any).connection;
if (!connection) {
setQuality('high');
return;
}
const { effectiveType, saveData } = connection;
if (saveData || effectiveType === 'slow-2g' || effectiveType === '2g') {
setQuality('low');
} else {
setQuality('high');
}
};
updateQuality();
window.addEventListener('online', updateQuality);
window.addEventListener('offline', updateQuality);
const connection = (navigator as any).connection;
if (connection) {
connection.addEventListener('change', updateQuality);
}
return () => {
window.removeEventListener('online', updateQuality);
window.removeEventListener('offline', updateQuality);
if (connection) {
connection.removeEventListener('change', updateQuality);
}
};
}, []);
return quality;
}
// Usage
function AdaptiveContent() {
const quality = useNetworkAwareDegradation();
if (quality === 'offline') {
return <OfflineContent />;
}
if (quality === 'low') {
return <LowQualityContent />; // Smaller images, less data
}
return <HighQualityContent />; // Full experience
}Degradation Levels
// Define degradation tiers
type DegradationLevel = 'full' | 'partial' | 'minimal' | 'offline';
interface AppState {
degradationLevel: DegradationLevel;
features: {
analytics: boolean;
recommendations: boolean;
realtime: boolean;
highQualityImages: boolean;
};
}
function getDegradationLevel(): AppState {
const online = navigator.onLine;
const connection = (navigator as any).connection;
if (!online) {
return {
degradationLevel: 'offline',
features: {
analytics: false,
recommendations: false,
realtime: false,
highQualityImages: false,
},
};
}
// Check service availability
const servicesHealth = checkServicesHealth();
if (servicesHealth.critical < 0.5) {
return {
degradationLevel: 'minimal',
features: {
analytics: false,
recommendations: false,
realtime: false,
highQualityImages: false,
},
};
}
if (servicesHealth.important < 0.7) {
return {
degradationLevel: 'partial',
features: {
analytics: true,
recommendations: false,
realtime: false,
highQualityImages: true,
},
};
}
return {
degradationLevel: 'full',
features: {
analytics: true,
recommendations: true,
realtime: true,
highQualityImages: true,
},
};
}Best Practices
- Prioritize Core Features: Keep essential features working
- Use Cached Data: Better than nothing
- Inform Users: Show when degraded
- Progressive Enhancement: Start simple, add features
- Test Degraded States: Simulate failures
- Fail Independently: One feature fails ≠ all fail
- Provide Alternatives: Offer different ways to accomplish tasks
Common Pitfalls
❌ All or nothing: Everything fails together
✅ Independent features
❌ Silent degradation: User doesn't know what's happening
✅ Show degradation notices
❌ No fallback data: Showing nothing
✅ Use cached/default data
❌ Breaking core features: Payment, auth fail
✅ Protect critical paths
Degradation Hierarchy
- Critical (Never degrade): Auth, payments, data integrity
- Important (Degrade gracefully): User profile, checkout flow
- Nice-to-have (Degrade freely): Analytics, recommendations, animations
- Optional (Disable first): Real-time updates, high-quality images
Graceful degradation is about resilience—keeping your app useful even when things go wrong.