Features: - Zero configuration, just works out of the box - Runtime type inference and validation - Built-in caching with W-TinyLFU algorithm - Automatic retries with exponential backoff - Circuit breaker for resilience - Request deduplication - Offline support with queue - OpenAPI schema discovery - Full TypeScript support with type descriptors - Modular architecture - Configurable for advanced use cases Built with bun, ready for npm publishing
34 KiB
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:
// 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:
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) ✅
// 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) 🔄
// 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) 🚫
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) 🔥
// 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:
// 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:
// 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:
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 (
<div className="error-container">
<div className="error-icon">{userError.icon}</div>
<h3>{userError.title}</h3>
<p>{userError.message}</p>
{userError.countdown && (
<CountdownTimer seconds={userError.countdown} />
)}
<div className="error-actions">
{userError.actions?.map(action => (
<button
key={action.action}
onClick={() => onAction(action.action)}
>
{action.label}
</button>
))}
</div>
</div>
)
}
Weather Buddy 5.0: Bulletproof Edition
Let's make Weather Buddy handle every possible failure:
<!DOCTYPE html>
<html>
<head>
<title>Weather Buddy 5.0 - Bulletproof Edition</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.city-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
.city-card { border: 1px solid #ddd; padding: 15px; border-radius: 8px; }
.city-card.error { border-color: #ff4444; background: #fff0f0; }
.city-card.loading { opacity: 0.6; }
.city-card.offline { border-color: #ffaa00; background: #fffaf0; }
.error-message { color: #ff4444; padding: 10px; background: #fff0f0; border-radius: 4px; margin: 10px 0; }
.retry-button { background: #4CAF50; color: white; border: none; padding: 5px 10px; cursor: pointer; }
.status-bar { position: fixed; top: 0; left: 0; right: 0; padding: 10px; text-align: center; }
.status-bar.online { background: #4CAF50; color: white; }
.status-bar.offline { background: #ff9800; color: white; }
.circuit-status { position: fixed; bottom: 20px; right: 20px; padding: 10px; background: white; border: 1px solid #ddd; border-radius: 4px; }
</style>
<script type="module">
import { tf } from 'https://esm.sh/typedfetch'
// Global error tracking
const errorStats = new Map()
const retryQueues = new Map()
let isOnline = navigator.onLine
// Configure TypedFetch with custom error handling
tf.addResponseInterceptor(response => {
// Track successful requests
if (response.config?.url) {
errorStats.delete(response.config.url)
}
return response
})
tf.addRequestInterceptor(config => {
// Add request ID for tracking
config.headers = {
...config.headers,
'X-Request-ID': crypto.randomUUID()
}
return config
})
// Network status monitoring
window.addEventListener('online', () => {
isOnline = true
updateNetworkStatus()
processRetryQueue()
})
window.addEventListener('offline', () => {
isOnline = false
updateNetworkStatus()
})
function updateNetworkStatus() {
const statusBar = document.getElementById('statusBar')
if (isOnline) {
statusBar.className = 'status-bar online'
statusBar.textContent = '🟢 Online'
} else {
statusBar.className = 'status-bar offline'
statusBar.textContent = '🔴 Offline - Working in offline mode'
}
}
// Smart error handling with recovery
async function fetchWeatherWithRecovery(city, retryCount = 0) {
const cardId = `city-${city.replace(/\s/g, '-')}`
const card = document.getElementById(cardId)
try {
// Show loading state
card.classList.add('loading')
card.classList.remove('error', 'offline')
// Check if we're offline
if (!isOnline) {
throw { code: 'OFFLINE', message: 'No internet connection' }
}
// Attempt to fetch weather
const { data } = await tf.get(`https://wttr.in/${city}?format=j1`, {
timeout: 10000,
retry: {
attempts: 3,
delay: 1000,
backoff: 'exponential'
}
})
// Success! Update the card
updateWeatherCard(card, city, data)
// Clear any error tracking
errorStats.delete(city)
} catch (error) {
console.error(`Weather fetch failed for ${city}:`, error)
// Track errors
const errorCount = (errorStats.get(city) || 0) + 1
errorStats.set(city, errorCount)
// Handle different error types
handleWeatherError(card, city, error, errorCount, retryCount)
} finally {
card.classList.remove('loading')
}
}
function handleWeatherError(card, city, error, errorCount, retryCount) {
let errorDisplay = {
icon: '❌',
title: 'Error',
message: 'Something went wrong',
actions: []
}
// Offline error
if (error.code === 'OFFLINE' || !navigator.onLine) {
card.classList.add('offline')
errorDisplay = {
icon: '📡',
title: 'Offline',
message: 'Waiting for connection...',
actions: []
}
// Queue for retry when online
if (!retryQueues.has(city)) {
retryQueues.set(city, () => fetchWeatherWithRecovery(city))
}
}
// Network error
else if (error.code === 'NETWORK_ERROR') {
errorDisplay = {
icon: '🌐',
title: 'Connection Problem',
message: 'Check your internet',
actions: [{ label: 'Retry', action: () => fetchWeatherWithRecovery(city) }]
}
}
// Timeout
else if (error.code === 'TIMEOUT') {
errorDisplay = {
icon: '⏱️',
title: 'Too Slow',
message: 'Server took too long',
actions: [{ label: 'Try Again', action: () => fetchWeatherWithRecovery(city) }]
}
}
// Rate limiting
else if (error.response?.status === 429) {
const retryAfter = error.response.headers.get('Retry-After') || 60
errorDisplay = {
icon: '🚦',
title: 'Rate Limited',
message: `Wait ${retryAfter}s`,
countdown: parseInt(retryAfter)
}
// Auto-retry after cooldown
setTimeout(() => fetchWeatherWithRecovery(city), retryAfter * 1000)
}
// Server errors - implement exponential backoff
else if (error.response?.status >= 500) {
const nextRetry = Math.min(Math.pow(2, retryCount) * 1000, 60000)
errorDisplay = {
icon: '🔧',
title: 'Server Issue',
message: `Retry in ${Math.round(nextRetry/1000)}s`,
actions: [{ label: 'Retry Now', action: () => fetchWeatherWithRecovery(city, retryCount + 1) }]
}
// Auto-retry with backoff
setTimeout(() => fetchWeatherWithRecovery(city, retryCount + 1), nextRetry)
}
// Not found
else if (error.response?.status === 404) {
card.classList.add('error')
errorDisplay = {
icon: '🔍',
title: 'City Not Found',
message: 'Check the city name',
actions: [{ label: 'Remove', action: () => removeCity(city) }]
}
}
// Update card with error display
card.innerHTML = `
<h3>${city}</h3>
<div class="error-display">
<div style="font-size: 48px; text-align: center;">${errorDisplay.icon}</div>
<h4>${errorDisplay.title}</h4>
<p>${errorDisplay.message}</p>
${errorDisplay.countdown ? `<div class="countdown" data-seconds="${errorDisplay.countdown}"></div>` : ''}
<div class="error-actions">
${errorDisplay.actions.map(action =>
`<button class="retry-button" onclick="window.errorActions['${city}']()">
${action.label}
</button>`
).join('')}
</div>
</div>
<div class="error-stats">
<small>Errors: ${errorCount} | Circuit: ${getCircuitStatus(city)}</small>
</div>
`
// Store action handlers
if (!window.errorActions) window.errorActions = {}
if (errorDisplay.actions[0]) {
window.errorActions[city] = errorDisplay.actions[0].action
}
// Start countdown if needed
if (errorDisplay.countdown) {
startCountdown(card.querySelector('.countdown'), errorDisplay.countdown)
}
}
function updateWeatherCard(card, city, weather) {
card.classList.remove('error', 'offline')
card.innerHTML = `
<h3>${city}</h3>
<button onclick="removeCity('${city}')" style="float: right">×</button>
<p>🌡️ ${weather.current_condition[0].temp_C}°C / ${weather.current_condition[0].temp_F}°F</p>
<p>🌤️ ${weather.current_condition[0].weatherDesc[0].value}</p>
<p>💨 Wind: ${weather.current_condition[0].windspeedKmph} km/h</p>
<p>💧 Humidity: ${weather.current_condition[0].humidity}%</p>
<p>🔄 Updated: ${new Date().toLocaleTimeString()}</p>
<button class="retry-button" onclick="window.fetchWeatherWithRecovery('${city}')">
Refresh
</button>
`
}
function processRetryQueue() {
console.log('Processing retry queue...')
for (const [city, retryFn] of retryQueues.entries()) {
retryFn()
retryQueues.delete(city)
}
}
function getCircuitStatus(endpoint) {
const metrics = tf.getMetrics()
const circuit = metrics.circuitBreaker?.[endpoint]
if (!circuit) return 'closed'
return circuit.state
}
function updateCircuitDisplay() {
const display = document.getElementById('circuitStatus')
const metrics = tf.getMetrics()
const circuits = metrics.circuitBreaker || {}
const html = Object.entries(circuits)
.filter(([_, data]) => data.state !== 'closed')
.map(([endpoint, data]) => `
<div>
<strong>${endpoint}</strong>: ${data.state}
<br>Failures: ${data.failures}
</div>
`).join('')
display.innerHTML = html || '<div>All circuits healthy ✅</div>'
}
function startCountdown(element, seconds) {
let remaining = seconds
const update = () => {
element.textContent = `Retry in ${remaining}s`
if (remaining > 0) {
remaining--
setTimeout(update, 1000)
}
}
update()
}
// Global functions
window.fetchWeatherWithRecovery = fetchWeatherWithRecovery
window.addCity = async function(cityName) {
const cityDiv = document.createElement('div')
cityDiv.className = 'city-card'
cityDiv.id = `city-${cityName.replace(/\s/g, '-')}`
document.getElementById('cityGrid').appendChild(cityDiv)
await fetchWeatherWithRecovery(cityName)
}
window.removeCity = function(cityName) {
document.getElementById(`city-${cityName.replace(/\s/g, '-')}`).remove()
errorStats.delete(cityName)
retryQueues.delete(cityName)
}
// Initialize
window.addEventListener('load', () => {
updateNetworkStatus()
// Add default cities
['London', 'Tokyo', 'New York'].forEach(city => addCity(city))
// Update circuit breaker display
setInterval(updateCircuitDisplay, 2000)
// Enable debug mode for development
if (location.hostname === 'localhost') {
tf.enableDebug()
}
})
</script>
</head>
<body>
<div id="statusBar" class="status-bar online">🟢 Online</div>
<h1>Weather Buddy 5.0 - Bulletproof Edition 🛡️</h1>
<div class="search-box">
<input
type="text"
id="citySearch"
placeholder="Add a city..."
onkeypress="if(event.key==='Enter') addCity(this.value)"
/>
<button onclick="addCity(document.getElementById('citySearch').value)">Add City</button>
</div>
<div id="cityGrid" class="city-grid"></div>
<div id="circuitStatus" class="circuit-status">
<strong>Circuit Breakers</strong>
<div>All circuits healthy ✅</div>
</div>
</body>
</html>
Advanced Error Handling Patterns
1. Graceful Degradation
Fall back to cached or default data:
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:
// 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 (
<ErrorFallback
error={this.state.error}
resetError={() => 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
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
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:
// 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
// ❌ 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
// ❌ 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
// 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
// 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:
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:
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:
class OfflineQueue {
// Your code here:
// - Queue failed requests
// - Persist to localStorage
// - Retry when online
// - Handle conflicts
}
Key Takeaways 🎯
- Errors are inevitable - Plan for them from the start
- Different errors need different handling - Network vs 4xx vs 5xx
- TypedFetch provides smart errors - Use the suggestions
- Retry with exponential backoff - Don't hammer failing services
- Circuit breakers prevent cascading failures - Fail fast when needed
- User-friendly messages matter - Turn tech errors into helpful guidance
- Always have a fallback - Cached data is better than no data
- Test error scenarios - They happen more than success cases
Common Pitfalls 🚨
- Swallowing errors silently - Always inform the user
- Infinite retry loops - Set max attempts
- Not respecting rate limits - Honor Retry-After headers
- Generic error messages - Be specific and helpful
- No offline handling - Apps should work without internet
- 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.