Claude-skill-registry frontend-network-resilience
Network resilience patterns for frontend - retry logic, exponential backoff, error handling, offline detection
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/frontend-network-resilience" ~/.claude/skills/majiayu000-claude-skill-registry-frontend-network-resilience && rm -rf "$T"
skills/data/frontend-network-resilience/SKILL.mdFrontend Network Resilience Patterns
Comprehensive patterns for building robust network operations in frontend applications.
Core Principles
- Never trust the network - Always plan for failure
- Fail gracefully - Degrade functionality, don't break completely
- Provide feedback - User should know what's happening
- Auto-recover - Retry transient failures automatically
- Differentiate errors - Different errors need different messages
Pattern 1: Exponential Backoff Retry
When to Use:
- Loading external SDKs (Clerk, Stripe, Google Maps)
- API calls to backend services
- Third-party API integrations
- CDN resource loading
When NOT to Use:
- User-initiated actions (use immediate retry with button instead)
- Real-time operations (WebSocket - use reconnection logic instead)
- Operations that must succeed exactly once (payments - use idempotency)
Implementation Pattern:
/** * Retry an operation with exponential backoff * * @param operation - Async function to retry * @param maxAttempts - Maximum number of attempts (default: 4) * @param initialDelay - Initial delay in ms (default: 2000) * @param maxDelay - Maximum delay in ms (default: 30000) * @returns Result of operation or throws after all attempts exhausted */ async function retryWithBackoff<T>( operation: () => Promise<T>, maxAttempts = 4, initialDelay = 2000, maxDelay = 30000 ): Promise<T> { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await operation(); } catch (error) { // Last attempt - rethrow error if (attempt === maxAttempts) { throw error; } // Calculate delay with exponential backoff const delay = Math.min( initialDelay * Math.pow(2, attempt - 1), maxDelay ); console.log(`Attempt ${attempt}/${maxAttempts} failed, retrying in ${delay}ms...`); // Wait before next attempt await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error('All retry attempts exhausted'); }
Usage Example:
// Loading external SDK with retry const sdkLoaded = await retryWithBackoff( async () => { if (!sdk.loaded) { throw new Error('SDK not ready'); } return true; }, 4, // 4 attempts 2000, // Start with 2s delay 30000 // Max 30s delay );
Backoff Schedule Examples:
4 attempts, 2s initial: Attempt 1 → wait 2s → Attempt 2 → wait 4s → Attempt 3 → wait 8s → Attempt 4 Total: ~14s 4 attempts, 3s initial: Attempt 1 → wait 3s → Attempt 2 → wait 6s → Attempt 3 → wait 12s → Attempt 4 Total: ~21s 5 attempts, 2s initial: Attempt 1 → wait 2s → Attempt 2 → wait 4s → Attempt 3 → wait 8s → Attempt 4 → wait 16s → Attempt 5 Total: ~30s
Pattern 2: Error Type Differentiation
Error Taxonomy:
type NetworkErrorType = | 'offline' // No internet connection | 'timeout' // Request/load took too long | 'blocked' // Blocked by browser/extension/firewall | 'not_found' // Resource doesn't exist (404) | 'server_error' // Server returned 5xx | 'unauthorized' // Auth failed (401/403) | 'rate_limited' // Too many requests (429) | 'network_error' // Generic network failure | 'unknown'; // Unclassified error interface NetworkError { type: NetworkErrorType; message: string; userMessage: string; // User-friendly message canRetry: boolean; // Should we offer retry? autoRetry: boolean; // Should we auto-retry? }
Error Detection:
function classifyNetworkError(error: any): NetworkError { // Offline detection if (!navigator.onLine) { return { type: 'offline', message: 'Network offline', userMessage: 'No internet connection. Please check your network.', canRetry: true, autoRetry: false // Don't auto-retry if offline }; } // Timeout if (error.name === 'TimeoutError' || error.message?.includes('timeout')) { return { type: 'timeout', message: error.message, userMessage: 'Connection is very slow. Please check your internet.', canRetry: true, autoRetry: true // Auto-retry timeouts }; } // Blocked (CSP, ad blocker, CORS) if (error.message?.includes('blocked') || error.message?.includes('CSP') || error.name === 'SecurityError') { return { type: 'blocked', message: error.message, userMessage: 'Resource blocked. Please disable ad blockers.', canRetry: true, autoRetry: false // User must fix blocker first }; } // HTTP status codes if (error.status === 404) { return { type: 'not_found', message: error.message, userMessage: 'Resource not found.', canRetry: false, autoRetry: false }; } if (error.status >= 500) { return { type: 'server_error', message: error.message, userMessage: 'Server error. Please try again later.', canRetry: true, autoRetry: true // Server errors are often transient }; } if (error.status === 401 || error.status === 403) { return { type: 'unauthorized', message: error.message, userMessage: 'Please sign in again.', canRetry: false, autoRetry: false }; } if (error.status === 429) { return { type: 'rate_limited', message: error.message, userMessage: 'Too many requests. Please wait a moment.', canRetry: true, autoRetry: true // With longer backoff }; } // Generic network error return { type: 'network_error', message: error.message || 'Unknown error', userMessage: 'Network error. Please try again.', canRetry: true, autoRetry: true }; }
Pattern 3: Offline Detection
Implementation:
import { onMount } from 'svelte'; let isOnline = $state(true); onMount(() => { // Initial check isOnline = navigator.onLine; // Listen for online/offline events const handleOnline = () => { isOnline = true; }; const handleOffline = () => { isOnline = false; }; window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; });
Usage in Components:
{#if !isOnline} <div class="offline-banner"> ⚠️ No internet connection. Some features may not work. </div> {/if} <button onclick={handleAction} disabled={!isOnline} > Submit </button>
Pattern 4: Progressive Loading with Feedback
For External SDK Loading:
<script lang="ts"> let loadingState = $state<'idle' | 'loading' | 'ready' | 'error'>('idle'); let loadingTime = $state(0); let attemptNumber = $state(0); let error = $state<NetworkError | null>(null); // Show progress message after 3s let showProgress = $derived(loadingTime > 3); async function loadSDK() { loadingState = 'loading'; loadingTime = 0; // Update loading time every second const interval = setInterval(() => { loadingTime++; }, 1000); try { await retryWithBackoff( async () => { attemptNumber++; // SDK load logic if (!sdk.loaded) throw new Error('Not ready'); return true; }, 4, 2000 ); clearInterval(interval); loadingState = 'ready'; } catch (err) { clearInterval(interval); loadingState = 'error'; error = classifyNetworkError(err); } } </script> {#if loadingState === 'loading'} <div class="loading-container"> <Spinner /> {#if attemptNumber > 1} <p>Connecting... (attempt {attemptNumber}/4)</p> {:else if showProgress} <p>This is taking longer than expected ({loadingTime}s)...</p> {:else} <p>Loading...</p> {/if} </div> {:else if loadingState === 'error'} <ErrorMessage error={error} onRetry={error.canRetry ? loadSDK : undefined} /> {/if}
Pattern 5: SSR-Friendly Network Operations
Guidelines:
-
Never call network operations during SSR
// ❌ BAD - Will fail during SSR const data = await fetch('/api/data'); // ✅ GOOD - Only runs client-side onMount(async () => { const data = await fetch('/api/data'); }); -
Provide initial loading state in SSR HTML
<!-- This renders during SSR --> <div class="content"> {#if data} <DataDisplay {data} /> {:else} <!-- This spinner is in SSR HTML --> <Spinner /> <p>Loading content...</p> {/if} </div> -
Handle hydration gracefully
let data = $state(null); let isLoading = $state(true); // true during SSR and hydration onMount(async () => { // This runs after hydration data = await loadData(); isLoading = false; });
Planning Checklist
When planning any feature with network operations, ensure:
Network Operation Requirements
- Retry logic with exponential backoff specified
- Number of attempts and timing justified (e.g., 4 attempts, 2s initial)
- Error types are differentiated with appropriate messages
- Offline state is detected and handled
- Timeout values are specified and justified
- Manual retry option provided (button)
- Loading states defined for all durations (0-3s, 3-10s, 10s+)
SSR/Hydration Requirements
- Network operations only in onMount or client-side code
- Initial loading state present in SSR HTML
- No flash of incorrect content (FOUC) during hydration
- Progressive enhancement considered
UX Requirements
- User sees feedback within 1s (spinner, progress indicator)
- Progress message shown if loading > 3s
- Error messages are user-friendly and actionable
- User can retry without full page refresh
- Graceful degradation if JS disabled (where possible)
Testing Requirements
All network operations must include tests for:
- Success scenario - Normal network conditions
- Slow network - Simulated 3G (3-5s response time)
- Very slow network - Extreme latency (8-10s)
- Offline - No internet connection
- Timeout - No response within timeout period
- Intermittent failure - Fails first 2 attempts, succeeds on 3rd
- Persistent failure - Fails all retry attempts
- Error types - 401, 403, 404, 429, 500, 503
Playwright Example:
test('SDK loads with retry on slow network', async ({ page, context }) => { // Simulate slow 3G await context.route('**/clerk.sdk.js', async (route) => { await new Promise(resolve => setTimeout(resolve, 4000)); await route.continue(); }); await page.goto('/login'); // Should show loading state await expect(page.locator('.spinner')).toBeVisible(); // Should show progress message after 3s await expect(page.locator('text=Taking longer')).toBeVisible({ timeout: 4000 }); // Should eventually load await expect(page.locator('.clerk-sign-in')).toBeVisible({ timeout: 10000 }); });
Common Mistakes to Avoid
❌ Mistake 1: Single Timeout Without Retry
// BAD - Gives up after one timeout const loaded = await waitForSDK(10000); if (!loaded) { showError('Failed to load'); }
// GOOD - Retries with backoff const loaded = await retryWithBackoff( () => waitForSDK(5000), 4, 2000 );
❌ Mistake 2: Generic Error Messages
// BAD - Same message for all errors catch (error) { showError('Something went wrong. Please try again.'); }
// GOOD - Differentiated messages catch (error) { const networkError = classifyNetworkError(error); showError(networkError.userMessage); if (networkError.canRetry) { showRetryButton(); } }
❌ Mistake 3: Ignoring Offline State
// BAD - Attempts network call while offline async function loadData() { return await fetch('/api/data'); }
// GOOD - Checks offline state first async function loadData() { if (!navigator.onLine) { throw new Error('offline'); } return await fetch('/api/data'); }
❌ Mistake 4: Network Calls During SSR
// BAD - Will fail during SSR const data = await fetch('/api/data');
// GOOD - Only client-side let data = $state(null); onMount(async () => { data = await fetch('/api/data'); });
References
- MDN: Exponential Backoff
- Google Cloud: Retry Strategy
- AWS: Error Retries and Exponential Backoff
- SvelteKit: Load Functions
Last Updated: 2025-11-06 Applies To: All frontend network operations Mandatory: YES - Plan agents must reference this during planning