# Chapter 5: Error Handling Like a Pro *"The difference between a good developer and a great developer is how they handle failure."* --- ## When Everything Goes Wrong Sarah's Weather Buddy app was growing fast. Hundreds of users were saving their favorite cities and sharing dashboards. Then, one Friday afternoon, everything broke. "The app is down!" her colleague Jake shouted. "I'm getting weird errors everywhere!" Sarah looked at the error messages: - "NetworkError when attempting to fetch resource" - "Failed to fetch" - "TypeError: Cannot read property 'data' of undefined" Not very helpful. "This is why error handling matters," Marcus said, walking over. "Let's make your app bulletproof." ## The Anatomy of API Errors API errors come in many flavors, each requiring different handling: ```javascript // 1. Network Errors - Can't reach the server try { await tf.get('https://api.example.com/data') } catch (error) { if (error.code === 'NETWORK_ERROR') { console.log('Check your internet connection') } } // 2. HTTP Errors - Server says "no" try { await tf.get('https://api.example.com/secret') } catch (error) { if (error.response?.status === 401) { console.log('You need to login first') } } // 3. Timeout Errors - Server is too slow try { await tf.get('https://slow-api.example.com/data', { timeout: 5000 // 5 seconds }) } catch (error) { if (error.code === 'TIMEOUT') { console.log('Server is taking too long') } } // 4. Parse Errors - Bad response format try { await tf.get('https://api.example.com/broken') } catch (error) { if (error.code === 'PARSE_ERROR') { console.log('Server sent invalid data') } } ``` ## TypedFetch's Smart Error System TypedFetch doesn't just catch errors - it helps you fix them: ```javascript try { const { data } = await tf.get('https://api.github.com/user') } catch (error) { console.log(error.message) // "Authentication required" console.log(error.suggestions) // ["Add an Authorization header", "Get a token from Settings"] console.log(error.code) // "AUTH_REQUIRED" console.log(error.status) // 401 // Debug mode gives even more info if (error.debug) { error.debug() // Outputs: // Request URL: https://api.github.com/user // Method: GET // Headers: { ... } // Response Status: 401 Unauthorized // Response Headers: { ... } } } ``` ## HTTP Status Codes: What They Really Mean ### The Good (2xx) ✅ ```javascript // 200 OK - Everything worked const { data } = await tf.get('/api/users') // 201 Created - New resource created const { data: newUser } = await tf.post('/api/users', { data: { name: 'Sarah' } }) // 204 No Content - Success, but no data to return await tf.delete('/api/users/123') ``` ### The Redirects (3xx) 🔄 ```javascript // TypedFetch follows redirects automatically const { data, response } = await tf.get('/api/old-endpoint') console.log(response.url) // 'https://api.example.com/new-endpoint' ``` ### The Client Errors (4xx) 🚫 ```javascript try { await tf.get('/api/users/999999') } catch (error) { switch (error.response?.status) { case 400: console.error('Bad Request - Check your data') break case 401: console.error('Unauthorized - Login required') // Redirect to login window.location.href = '/login' break case 403: console.error('Forbidden - You don\'t have permission') break case 404: console.error('Not Found - Resource doesn\'t exist') break case 409: console.error('Conflict - Resource already exists') break case 422: console.error('Validation Error') console.error(error.data?.errors) // Field-specific errors break case 429: console.error('Too Many Requests - Slow down!') const retryAfter = error.response.headers.get('Retry-After') console.log(`Try again in ${retryAfter} seconds`) break } } ``` ### The Server Errors (5xx) 🔥 ```javascript // These are usually temporary try { await tf.get('/api/data') } catch (error) { if (error.response?.status >= 500) { console.error('Server error - not your fault!') // TypedFetch automatically retries 5xx errors // But you can handle them manually too if (error.response.status === 503) { console.log('Service temporarily unavailable') } } } ``` ## Retry Strategies: Never Give Up (Too Easily) TypedFetch includes smart retry logic, but you can customize it: ```javascript // Default retry behavior (for 5xx and network errors) const { data } = await tf.get('/api/flaky-endpoint') // Custom retry configuration const { data } = await tf.get('/api/important-data', { retry: { attempts: 5, // Try 5 times (default: 3) delay: 1000, // Start with 1 second delay maxDelay: 30000, // Max 30 seconds between retries backoff: 'exponential', // Double delay each time retryOn: [500, 502, 503, 504, 'NETWORK_ERROR', 'TIMEOUT'] } }) // Disable retries const { data } = await tf.get('/api/no-retry', { retry: false }) // Manual retry with exponential backoff async function fetchWithRetry(url, maxAttempts = 3) { let lastError for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await tf.get(url) } catch (error) { lastError = error // Don't retry client errors if (error.response?.status >= 400 && error.response?.status < 500) { throw error } if (attempt < maxAttempts) { // Exponential backoff with jitter const delay = Math.min(1000 * Math.pow(2, attempt - 1), 30000) const jitter = Math.random() * 0.3 * delay console.log(`Retry ${attempt}/${maxAttempts} in ${delay + jitter}ms`) await new Promise(resolve => setTimeout(resolve, delay + jitter)) } } } throw lastError } ``` ## Circuit Breaker Pattern: Fail Fast When a service is down, stop hammering it: ```javascript // TypedFetch includes a built-in circuit breaker const { data } = await tf.get('/api/unreliable-service') // If too many requests fail, the circuit "opens" // and TypedFetch will fail fast without making requests // You can check circuit status const metrics = tf.getMetrics() console.log(metrics.circuitBreaker) // { // '/api/unreliable-service': { // state: 'open', // failures: 10, // lastFailure: '2024-01-20T15:30:00Z', // nextRetry: '2024-01-20T15:35:00Z' // } // } // Reset a tripped circuit manually tf.resetCircuitBreaker('/api/unreliable-service') // Or implement your own circuit breaker class CircuitBreaker { constructor(threshold = 5, timeout = 60000) { this.threshold = threshold this.timeout = timeout this.failures = 0 this.lastFailureTime = null this.state = 'closed' // closed, open, half-open } async execute(fn) { if (this.state === 'open') { if (Date.now() - this.lastFailureTime > this.timeout) { this.state = 'half-open' } else { throw new Error('Circuit breaker is open') } } try { const result = await fn() this.onSuccess() return result } catch (error) { this.onFailure() throw error } } onSuccess() { this.failures = 0 this.state = 'closed' } onFailure() { this.failures++ this.lastFailureTime = Date.now() if (this.failures >= this.threshold) { this.state = 'open' console.log('Circuit breaker opened!') } } } // Usage const breaker = new CircuitBreaker() try { const data = await breaker.execute(() => tf.get('/api/flaky')) } catch (error) { console.error('Service unavailable') } ``` ## User-Friendly Error Messages Turn technical errors into helpful messages: ```javascript function getUserMessage(error) { // Network errors if (error.code === 'NETWORK_ERROR') { return { title: 'Connection Problem', message: 'Please check your internet connection and try again.', icon: '📡', actions: [ { label: 'Retry', action: 'retry' }, { label: 'Work Offline', action: 'offline' } ] } } // Authentication errors if (error.response?.status === 401) { return { title: 'Please Sign In', message: 'You need to sign in to access this feature.', icon: '🔒', actions: [ { label: 'Sign In', action: 'login' }, { label: 'Create Account', action: 'register' } ] } } // Rate limiting if (error.response?.status === 429) { const retryAfter = error.response.headers.get('Retry-After') return { title: 'Slow Down!', message: `You're making too many requests. Please wait ${retryAfter} seconds.`, icon: '⏱️', countdown: parseInt(retryAfter) } } // Server errors if (error.response?.status >= 500) { return { title: 'Server Problem', message: 'Something went wrong on our end. We\'re working on it!', icon: '🔧', actions: [ { label: 'Try Again', action: 'retry' }, { label: 'Report Issue', action: 'report' } ] } } // Generic error return { title: 'Something Went Wrong', message: error.message || 'An unexpected error occurred.', icon: '❌', actions: [ { label: 'Try Again', action: 'retry' } ] } } // React component example function ErrorDisplay({ error, onAction }) { const userError = getUserMessage(error) return (
{userError.icon}

{userError.title}

{userError.message}

{userError.countdown && ( )}
{userError.actions?.map(action => ( ))}
) } ``` ## Weather Buddy 5.0: Bulletproof Edition Let's make Weather Buddy handle every possible failure: ```html Weather Buddy 5.0 - Bulletproof Edition
🟢 Online

Weather Buddy 5.0 - Bulletproof Edition 🛡️

Circuit Breakers
All circuits healthy ✅
``` ## Advanced Error Handling Patterns ### 1. Graceful Degradation Fall back to cached or default data: ```javascript async function getWeatherWithFallback(city) { try { // Try live data first const { data } = await tf.get(`/api/weather/${city}`) return { data, source: 'live' } } catch (error) { // Try cache const cached = await getCachedWeather(city) if (cached) { return { data: cached, source: 'cache' } } // Use historical average const historical = await getHistoricalWeather(city) if (historical) { return { data: historical, source: 'historical' } } // Last resort: estimated data return { data: { temp: 20, condition: 'unknown', message: 'Unable to get current weather' }, source: 'estimated' } } } ``` ### 2. Error Boundaries Contain errors to prevent app crashes: ```javascript // React error boundary class ApiErrorBoundary extends React.Component { state = { hasError: false, error: null } static getDerivedStateFromError(error) { return { hasError: true, error } } componentDidCatch(error, errorInfo) { console.error('API Error:', error, errorInfo) // Report to error tracking service if (window.errorReporter) { window.errorReporter.log(error, { component: 'ApiErrorBoundary', ...errorInfo }) } } render() { if (this.state.hasError) { return ( this.setState({ hasError: false })} /> ) } return this.props.children } } // Vue error handler app.config.errorHandler = (err, instance, info) => { if (err.code?.startsWith('API_')) { // Handle API errors specially showApiError(err) } else { // Let other errors bubble up throw err } } ``` ### 3. Error Recovery Strategies ```javascript class ErrorRecovery { constructor() { this.strategies = new Map() } register(errorType, strategy) { this.strategies.set(errorType, strategy) } async handle(error, context) { // Try specific strategies first const strategy = this.strategies.get(error.code) || this.strategies.get(error.response?.status) || this.strategies.get('default') if (strategy) { return await strategy(error, context) } throw error } } // Register recovery strategies const recovery = new ErrorRecovery() recovery.register('NETWORK_ERROR', async (error, { retry }) => { // Wait for connection await waitForNetwork() return retry() }) recovery.register(401, async (error, { refresh }) => { // Try refreshing auth token const newToken = await refreshAuthToken() if (newToken) { return refresh() } throw error }) recovery.register(429, async (error, { retry }) => { // Honor rate limit const retryAfter = error.response.headers.get('Retry-After') || 60 await sleep(retryAfter * 1000) return retry() }) // Usage async function apiCallWithRecovery(url) { const context = { retry: () => tf.get(url), refresh: () => tf.get(url) } try { return await tf.get(url) } catch (error) { return await recovery.handle(error, context) } } ``` ### 4. Error Aggregation and Reporting ```javascript class ErrorReporter { constructor() { this.errors = [] this.threshold = 10 this.window = 60000 // 1 minute } report(error) { const now = Date.now() // Add to error list this.errors.push({ error, timestamp: now, url: error.config?.url, status: error.response?.status }) // Clean old errors this.errors = this.errors.filter(e => now - e.timestamp < this.window ) // Check if we should alert if (this.errors.length >= this.threshold) { this.sendAlert() } } sendAlert() { const summary = this.summarizeErrors() // Send to monitoring service fetch('/api/monitoring/errors', { method: 'POST', body: JSON.stringify(summary) }) // Show user warning if (summary.criticalCount > 5) { showSystemWarning('Multiple services are experiencing issues') } } summarizeErrors() { const byEndpoint = new Map() const byStatus = new Map() for (const error of this.errors) { // Group by endpoint const endpoint = error.url || 'unknown' byEndpoint.set(endpoint, (byEndpoint.get(endpoint) || 0) + 1) // Group by status const status = error.status || 'network' byStatus.set(status, (byStatus.get(status) || 0) + 1) } return { total: this.errors.length, byEndpoint: Object.fromEntries(byEndpoint), byStatus: Object.fromEntries(byStatus), criticalCount: byStatus.get(500) || 0 } } } // Global error reporter const errorReporter = new ErrorReporter() // Intercept all TypedFetch errors tf.addResponseInterceptor(response => { if (response.error) { errorReporter.report(response.error) } return response }) ``` ## Testing Error Scenarios Always test how your app handles failures: ```javascript // Mock different error scenarios function createErrorMock(status, message) { return { get: async () => { const error = new Error(message) error.response = { status } throw error } } } // Test suite describe('Error Handling', () => { test('handles network errors', async () => { const mock = createErrorMock(null, 'Network error') const result = await handleApiCall(mock) expect(result.fallback).toBe(true) }) test('handles auth errors', async () => { const mock = createErrorMock(401, 'Unauthorized') const result = await handleApiCall(mock) expect(result.redirectedToLogin).toBe(true) }) test('handles rate limits', async () => { const mock = createErrorMock(429, 'Too many requests') const start = Date.now() const result = await handleApiCall(mock) const duration = Date.now() - start expect(duration).toBeGreaterThan(1000) // Waited }) }) ``` ## Best Practices for Error Handling 🎯 ### 1. Be Specific ```javascript // ❌ Bad: Generic error catch (error) { alert('Error occurred') } // ✅ Good: Specific handling catch (error) { if (error.code === 'NETWORK_ERROR') { showOfflineMessage() } else if (error.response?.status === 401) { redirectToLogin() } else { showErrorDetails(error) } } ``` ### 2. Provide Solutions ```javascript // ❌ Bad: Just showing the error showError('Request failed') // ✅ Good: Actionable message showError({ message: 'Unable to save your changes', suggestions: [ 'Check your internet connection', 'Try refreshing the page', 'Contact support if the problem persists' ], actions: [ { label: 'Retry', onClick: retry }, { label: 'Save Offline', onClick: saveOffline } ] }) ``` ### 3. Log Everything ```javascript // Comprehensive error logging function logError(error, context) { const errorData = { timestamp: new Date().toISOString(), message: error.message, code: error.code, status: error.response?.status, url: error.config?.url, method: error.config?.method, requestId: error.config?.headers?.['X-Request-ID'], userId: getCurrentUserId(), context, stack: error.stack, userAgent: navigator.userAgent } // Local logging console.error('API Error:', errorData) // Remote logging (if online) if (navigator.onLine) { sendToLoggingService(errorData) } else { queueForLaterLogging(errorData) } } ``` ### 4. Fail Gracefully ```javascript // Always have a fallback async function getResilientData(endpoint) { const strategies = [ () => tf.get(endpoint), // Live data () => tf.get(endpoint, { cache: 'force' }), // Force cache () => getFromLocalStorage(endpoint), // Local storage () => getDefaultData(endpoint) // Default data ] for (const strategy of strategies) { try { return await strategy() } catch (error) { continue // Try next strategy } } // All strategies failed return { data: null, error: true } } ``` ## Practice Time! 🏋️ ### Exercise 1: Custom Error Handler Build a comprehensive error handler: ```javascript class ApiErrorHandler { // Your code here: // - Handle different error types // - Implement retry logic // - Track error patterns // - Provide user feedback } ``` ### Exercise 2: Circuit Breaker Implementation Create a circuit breaker from scratch: ```javascript class CircuitBreaker { // Your code here: // - Track failures // - Open circuit on threshold // - Half-open state // - Auto-recovery } ``` ### Exercise 3: Offline Queue Build a queue for offline requests: ```javascript class OfflineQueue { // Your code here: // - Queue failed requests // - Persist to localStorage // - Retry when online // - Handle conflicts } ``` ## Key Takeaways 🎯 1. **Errors are inevitable** - Plan for them from the start 2. **Different errors need different handling** - Network vs 4xx vs 5xx 3. **TypedFetch provides smart errors** - Use the suggestions 4. **Retry with exponential backoff** - Don't hammer failing services 5. **Circuit breakers prevent cascading failures** - Fail fast when needed 6. **User-friendly messages matter** - Turn tech errors into helpful guidance 7. **Always have a fallback** - Cached data is better than no data 8. **Test error scenarios** - They happen more than success cases ## Common Pitfalls 🚨 1. **Swallowing errors silently** - Always inform the user 2. **Infinite retry loops** - Set max attempts 3. **Not respecting rate limits** - Honor Retry-After headers 4. **Generic error messages** - Be specific and helpful 5. **No offline handling** - Apps should work without internet 6. **Missing error boundaries** - One error shouldn't crash everything ## What's Next? You're now an error-handling expert! But what about making your app fast? In Chapter 6, we'll explore TypedFetch's revolutionary caching system: - The W-TinyLFU algorithm that's 25% better than LRU - Automatic cache management - Cache warming strategies - Invalidation patterns - Offline-first architecture Ready to make your app lightning fast? See you in Chapter 6! ⚡ --- ## Chapter Summary - API errors come in many forms: network, HTTP status codes, timeouts, parsing - TypedFetch provides enhanced errors with messages, suggestions, and debug info - HTTP status codes tell you exactly what went wrong (4xx = your fault, 5xx = server's fault) - Implement retry strategies with exponential backoff for transient failures - Circuit breakers prevent cascading failures by failing fast - Turn technical errors into user-friendly messages with actionable solutions - Always have fallback strategies: cache, local storage, or default data - Weather Buddy 5.0 handles every error gracefully with retries, offline support, and clear feedback **Next Chapter Preview**: The Cache Revolution - How TypedFetch's W-TinyLFU algorithm makes your app incredibly fast while using less memory than traditional caches.