Practice and reinforce the concepts from Lesson 9
In this activity, you'll:
Total time: 60-75 minutes
Access the template here: Error Handling Template
git clone
and navigate to the template folderindex.html
in your browser to start!What you'll do: Pick your API error scenario to master Time: 5 minutes
Choose one of these error-prone scenarios to build resilient handling:
Handle PokéAPI failures and network issues
Build fault-tolerant character data fetching
Handle weather service interruptions
Handle errors across multiple different APIs
What you'll do: Build a system to categorize and handle any API error Time: 15 minutes
class UniversalAPIError extends Error {
constructor(message, errorDetails = {}) {
super(message);
this.name = 'UniversalAPIError';
this.type = errorDetails.type || 'UNKNOWN_ERROR';
this.code = errorDetails.code || 0;
this.isRetryable = errorDetails.isRetryable !== false; // Default to retryable
this.apiSource = errorDetails.apiSource || 'unknown';
this.timestamp = new Date().toISOString();
this.context = errorDetails.context || {};
}
}
class UniversalErrorClassifier {
static classify(error, response = null, apiSource = 'unknown') {
// Network errors (no response)
if (!response) {
return new UniversalAPIError(
'Connection failed - check your internet',
{
type: 'NETWORK_ERROR',
code: 0,
isRetryable: true,
apiSource,
context: { originalError: error.message }
}
);
}
// Classify by HTTP status code
const errorMap = {
400: {
type: 'CLIENT_ERROR',
message: 'Invalid request - check your input',
isRetryable: false,
suggestions: ['Verify your search terms', 'Check required parameters']
},
401: {
type: 'AUTH_ERROR',
message: 'Authentication failed - check your API key',
isRetryable: false,
suggestions: ['Verify API key is correct', 'Check if API key is active']
},
403: {
type: 'PERMISSION_ERROR',
message: 'Access forbidden - insufficient permissions',
isRetryable: false,
suggestions: ['Check API subscription level', 'Verify endpoint permissions']
},
404: {
type: 'NOT_FOUND',
message: 'Resource not found',
isRetryable: false,
suggestions: this.getNotFoundSuggestions(apiSource)
},
429: {
type: 'RATE_LIMIT',
message: 'Too many requests - please wait',
isRetryable: true,
suggestions: ['Wait before retrying', 'Consider caching responses']
},
500: {
type: 'SERVER_ERROR',
message: 'Server error - service temporarily unavailable',
isRetryable: true,
suggestions: ['Try again in a few moments', 'Check service status']
},
502: {
type: 'BAD_GATEWAY',
message: 'Service gateway error',
isRetryable: true,
suggestions: ['Service may be updating', 'Try again shortly']
},
503: {
type: 'SERVICE_UNAVAILABLE',
message: 'Service temporarily unavailable',
isRetryable: true,
suggestions: ['Service may be under maintenance', 'Check back later']
}
};
const errorInfo = errorMap[response.status] || {
type: 'UNKNOWN_ERROR',
message: `Unexpected error (${response.status})`,
isRetryable: true,
suggestions: ['Try again', 'Check network connection']
};
return new UniversalAPIError(errorInfo.message, {
type: errorInfo.type,
code: response.status,
isRetryable: errorInfo.isRetryable,
apiSource,
context: {
suggestions: errorInfo.suggestions,
url: response.url,
responseStatus: response.status
}
});
}
static getNotFoundSuggestions(apiSource) {
const suggestions = {
'pokemon': [
'Check Pokemon name spelling',
'Try lowercase names only',
'Use Pokemon number instead',
'Browse popular Pokemon: pikachu, charizard, blastoise'
],
'starwars': [
'Try a different character ID (1-83)',
'Check character exists in the films',
'Browse main characters: 1 (Luke), 4 (Vader), 5 (Leia)'
],
'weather': [
'Check city name spelling',
'Try "City, Country" format',
'Use major city names',
'Popular cities: London, Tokyo, New York'
],
'country': [
'Check country name spelling',
'Try full country name',
'Use ISO country codes',
'Examples: United States, Germany, Japan'
]
};
return suggestions[apiSource] || [
'Check your input format',
'Try different search terms',
'Verify the resource exists'
];
}
static getUserFriendlyMessage(error) {
const messages = {
NETWORK_ERROR: '🌐 Check your internet connection',
AUTH_ERROR: '🔑 API authentication issue',
PERMISSION_ERROR: '🚫 Access denied',
NOT_FOUND: '🔍 Item not found',
RATE_LIMIT: '⏱️ Too many requests',
SERVER_ERROR: '🔧 Service temporarily down',
BAD_GATEWAY: '🌉 Service gateway issue',
SERVICE_UNAVAILABLE: '⚠️ Service unavailable',
UNKNOWN_ERROR: '❓ Something went wrong'
};
return messages[error.type] || '❓ An unexpected error occurred';
}
static getActionableAdvice(error) {
if (!error.context?.suggestions) return [];
return error.context.suggestions;
}
}
// Test the universal error system
function testErrorClassification() {
console.log('Testing universal error classification...');
const testCases = [
{ status: 404, apiSource: 'pokemon', expected: 'NOT_FOUND' },
{ status: 429, apiSource: 'weather', expected: 'RATE_LIMIT' },
{ status: 500, apiSource: 'starwars', expected: 'SERVER_ERROR' },
{ status: null, apiSource: 'country', expected: 'NETWORK_ERROR' }
];
testCases.forEach(testCase => {
const mockResponse = testCase.status ? { status: testCase.status, url: 'test-url' } : null;
const error = UniversalErrorClassifier.classify(
new Error('Test error'),
mockResponse,
testCase.apiSource
);
console.log(`${testCase.apiSource} ${testCase.status || 'network'}:`, {
type: error.type,
retryable: error.isRetryable,
message: UniversalErrorClassifier.getUserFriendlyMessage(error),
suggestions: UniversalErrorClassifier.getActionableAdvice(error)
});
});
}
testErrorClassification();
Your task:
What you'll do: Create intelligent retry mechanisms with backoff Time: 20 minutes
class SmartRetryHandler {
constructor(options = {}) {
this.maxRetries = options.maxRetries || 3;
this.baseDelay = options.baseDelay || 1000;
this.maxDelay = options.maxDelay || 30000;
this.retryStrategies = {
'NETWORK_ERROR': { multiplier: 2, jitterPercent: 0.3 },
'RATE_LIMIT': { multiplier: 3, jitterPercent: 0.1, minDelay: 5000 },
'SERVER_ERROR': { multiplier: 2, jitterPercent: 0.4 },
'BAD_GATEWAY': { multiplier: 1.5, jitterPercent: 0.2 },
'SERVICE_UNAVAILABLE': { multiplier: 2.5, jitterPercent: 0.3 }
};
}
async executeWithRetry(asyncFunction, context = {}) {
let lastError;
const startTime = Date.now();
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
console.log(`🔄 Attempt ${attempt + 1}/${this.maxRetries + 1} for ${context.operation || 'API call'}`);
const result = await asyncFunction();
if (attempt > 0) {
console.log(`✅ Succeeded after ${attempt + 1} attempts in ${Date.now() - startTime}ms`);
}
return result;
} catch (error) {
lastError = error;
// Don't retry if it's the last attempt
if (attempt === this.maxRetries) {
console.log(`❌ Final failure after ${attempt + 1} attempts`);
break;
}
// Don't retry if error is not retryable
if (error.isRetryable === false) {
console.log(`❌ Error not retryable: ${error.type}`);
break;
}
// Calculate delay for this error type
const delay = this.calculateDelay(attempt, error.type);
console.log(`⏳ Waiting ${delay}ms before retry... (${error.type})`);
await this.delay(delay);
}
}
// Enhance the error with retry information
lastError.retryAttempts = this.maxRetries + 1;
lastError.totalTime = Date.now() - startTime;
throw lastError;
}
calculateDelay(attempt, errorType) {
const strategy = this.retryStrategies[errorType] || { multiplier: 2, jitterPercent: 0.3 };
// Exponential backoff with strategy-specific multiplier
let delay = this.baseDelay * Math.pow(strategy.multiplier, attempt);
// Apply minimum delay for specific error types
if (strategy.minDelay) {
delay = Math.max(delay, strategy.minDelay);
}
// Cap at maximum delay
delay = Math.min(delay, this.maxDelay);
// Add jitter to prevent thundering herd
const jitter = delay * strategy.jitterPercent * (Math.random() - 0.5);
delay = Math.floor(delay + jitter);
return Math.max(delay, 100); // Minimum 100ms delay
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Adaptive retry based on recent success/failure patterns
adaptRetryStrategy(errorHistory) {
const recentErrors = errorHistory.slice(-10); // Last 10 errors
const networkErrors = recentErrors.filter(e => e.type === 'NETWORK_ERROR').length;
if (networkErrors > 5) {
// Increase base delay for network issues
this.baseDelay = Math.min(this.baseDelay * 1.5, 5000);
console.log(`📈 Adapted base delay to ${this.baseDelay}ms due to network issues`);
} else if (networkErrors < 2) {
// Decrease base delay for stable connections
this.baseDelay = Math.max(this.baseDelay * 0.8, 1000);
console.log(`📉 Reduced base delay to ${this.baseDelay}ms for stable connection`);
}
}
}
// Global retry handler instance
const retryHandler = new SmartRetryHandler({
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000
});
async function fetchPokemonWithRetry(pokemonName, options = {}) {
const fetchFunction = async () => {
try {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName.toLowerCase()}`);
if (!response.ok) {
const error = UniversalErrorClassifier.classify(
new Error('Pokemon API Error'),
response,
'pokemon'
);
throw error;
}
const data = await response.json();
return {
name: data.name,
id: data.id,
stats: data.stats.map(s => s.base_stat),
types: data.types.map(t => t.type.name),
sprite: data.sprites.front_default
};
} catch (networkError) {
if (!networkError.type) {
const error = UniversalErrorClassifier.classify(networkError, null, 'pokemon');
throw error;
}
throw networkError;
}
};
try {
showLoadingState('pokemon', 'Fetching Pokemon data...');
const pokemonData = await retryHandler.executeWithRetry(fetchFunction, {
operation: `Fetch Pokemon: ${pokemonName}`
});
displayPokemonData(pokemonData);
hideLoadingState('pokemon');
return pokemonData;
} catch (error) {
console.error('Final Pokemon fetch error:', error);
displayUniversalError('pokemon', error);
hideLoadingState('pokemon');
throw error;
}
}
async function fetchCharacterWithRetry(characterId, options = {}) {
const fetchFunction = async () => {
try {
const response = await fetch(`https://swapi.dev/api/people/${characterId}/`);
if (!response.ok) {
const error = UniversalErrorClassifier.classify(
new Error('SWAPI Error'),
response,
'starwars'
);
throw error;
}
const data = await response.json();
return {
name: data.name,
height: data.height,
mass: data.mass,
birth_year: data.birth_year,
films: data.films.length
};
} catch (networkError) {
if (!networkError.type) {
const error = UniversalErrorClassifier.classify(networkError, null, 'starwars');
throw error;
}
throw networkError;
}
};
try {
showLoadingState('character', 'Fetching character data...');
const characterData = await retryHandler.executeWithRetry(fetchFunction, {
operation: `Fetch Character: ${characterId}`
});
displayCharacterData(characterData);
hideLoadingState('character');
return characterData;
} catch (error) {
console.error('Final character fetch error:', error);
displayUniversalError('character', error);
hideLoadingState('character');
throw error;
}
}
Your task:
What you'll do: Implement fallback strategies and graceful degradation Time: 15 minutes
class UniversalFallbackProvider {
constructor() {
this.fallbackData = {
pokemon: {
'pikachu': { name: 'pikachu', types: ['electric'], stats: [55, 40, 90, 35, 50, 50] },
'charizard': { name: 'charizard', types: ['fire', 'flying'], stats: [78, 84, 78, 85, 109, 85] },
'blastoise': { name: 'blastoise', types: ['water'], stats: [79, 83, 100, 85, 105, 105] }
},
starwars: {
'1': { name: 'Luke Skywalker', height: '172', mass: '77', birth_year: '19BBY' },
'4': { name: 'Darth Vader', height: '202', mass: '136', birth_year: '41.9BBY' },
'5': { name: 'Leia Organa', height: '150', mass: '49', birth_year: '19BBY' }
},
weather: {
'london': { name: 'London', temp: 15, humidity: 65, description: 'partly cloudy' },
'tokyo': { name: 'Tokyo', temp: 22, humidity: 70, description: 'overcast' },
'new york': { name: 'New York', temp: 20, humidity: 55, description: 'sunny' }
}
};
this.errorRecoveryStrategies = {
'NOT_FOUND': 'suggest_alternatives',
'NETWORK_ERROR': 'use_cache_or_fallback',
'RATE_LIMIT': 'delay_and_cache',
'SERVER_ERROR': 'fallback_data',
'AUTH_ERROR': 'guide_user'
};
}
async recoverFromError(error, originalRequest) {
const strategy = this.errorRecoveryStrategies[error.type];
switch (strategy) {
case 'suggest_alternatives':
return this.suggestAlternatives(error, originalRequest);
case 'use_cache_or_fallback':
return this.useCacheOrFallback(error, originalRequest);
case 'delay_and_cache':
return this.delayAndUseCache(error, originalRequest);
case 'fallback_data':
return this.provideFallbackData(error, originalRequest);
case 'guide_user':
return this.guideUserToFix(error, originalRequest);
default:
return this.defaultRecovery(error, originalRequest);
}
}
suggestAlternatives(error, originalRequest) {
const suggestions = UniversalErrorClassifier.getActionableAdvice(error);
const alternatives = this.getPopularAlternatives(error.apiSource);
return {
type: 'suggestions',
message: `${error.message}. Here are some suggestions:`,
suggestions: suggestions,
alternatives: alternatives,
canRetry: true
};
}
useCacheOrFallback(error, originalRequest) {
// Try cache first, then fallback data
const cachedData = this.tryGetFromCache(originalRequest);
if (cachedData) {
return {
type: 'cached_data',
data: cachedData,
message: 'Using cached data due to connection issues',
isStale: true
};
}
const fallbackData = this.getFallbackData(originalRequest);
if (fallbackData) {
return {
type: 'fallback_data',
data: fallbackData,
message: 'Using offline data due to connection issues',
isOffline: true
};
}
return this.defaultRecovery(error, originalRequest);
}
getFallbackData(request) {
const { apiSource, searchTerm } = request;
const sourceData = this.fallbackData[apiSource];
if (sourceData && sourceData[searchTerm.toLowerCase()]) {
return {
...sourceData[searchTerm.toLowerCase()],
isFallback: true,
fallbackReason: 'API unavailable'
};
}
return null;
}
getPopularAlternatives(apiSource) {
const alternatives = {
pokemon: ['pikachu', 'charizard', 'blastoise', 'venusaur', 'alakazam'],
starwars: [
{ id: '1', name: 'Luke Skywalker' },
{ id: '4', name: 'Darth Vader' },
{ id: '5', name: 'Leia Organa' }
],
weather: ['London', 'Tokyo', 'New York', 'Paris', 'Sydney'],
country: ['United States', 'Japan', 'Germany', 'France', 'Canada']
};
return alternatives[apiSource] || [];
}
tryGetFromCache(request) {
// This would integrate with your caching system
// Return cached data if available
return null; // Placeholder
}
defaultRecovery(error, originalRequest) {
return {
type: 'error',
message: error.message,
suggestions: UniversalErrorClassifier.getActionableAdvice(error),
canRetry: error.isRetryable,
retryDelay: error.type === 'RATE_LIMIT' ? 5000 : 2000
};
}
}
const fallbackProvider = new UniversalFallbackProvider();
async function fetchWithRecovery(fetchFunction, requestContext) {
try {
return await fetchFunction();
} catch (error) {
console.log('💫 Attempting error recovery...');
const recovery = await fallbackProvider.recoverFromError(error, requestContext);
switch (recovery.type) {
case 'cached_data':
case 'fallback_data':
console.log(`📱 Using ${recovery.type}: ${recovery.message}`);
displayDataWithWarning(recovery.data, recovery.message);
return recovery.data;
case 'suggestions':
console.log('💡 Providing suggestions for user');
displayErrorWithSuggestions(error, recovery);
throw error;
default:
console.log('❌ No recovery possible');
displayUniversalError(requestContext.target, error);
throw error;
}
}
}
// Enhanced Pokemon fetch with recovery
async function fetchPokemonWithRecovery(pokemonName) {
const requestContext = {
apiSource: 'pokemon',
searchTerm: pokemonName,
target: 'pokemon'
};
return fetchWithRecovery(
() => fetchPokemonWithRetry(pokemonName),
requestContext
);
}
Your task:
What you'll do: Create helpful error interfaces Time: 10 minutes
class UniversalErrorDisplay {
constructor() {
this.errorHistory = [];
this.maxHistorySize = 20;
}
displayError(containerId, error, options = {}) {
this.logError(error);
const container = document.getElementById(containerId);
if (!container) {
console.error(`Error container ${containerId} not found`);
return;
}
const errorHTML = this.createErrorHTML(error, options);
container.innerHTML = errorHTML;
container.className = `error-container ${error.type.toLowerCase().replace('_', '-')}`;
// Auto-hide non-critical errors after delay
if (options.autoHide && !this.isCriticalError(error)) {
setTimeout(() => this.hideError(containerId), options.autoHide);
}
}
createErrorHTML(error, options) {
const userMessage = UniversalErrorClassifier.getUserFriendlyMessage(error);
const suggestions = UniversalErrorClassifier.getActionableAdvice(error);
const canRetry = error.isRetryable && !options.disableRetry;
return `
<div class="error-content">
<div class="error-icon">${this.getErrorIcon(error.type)}</div>
<div class="error-message">
<h3>${userMessage}</h3>
<p>${error.message}</p>
</div>
</div>
${suggestions.length > 0 ? `
<div class="error-suggestions">
<h4>💡 Try this:</h4>
<ul>
${suggestions.map(suggestion => `<li>${suggestion}</li>`).join('')}
</ul>
</div>
` : ''}
<div class="error-actions">
${canRetry ? `
<button class="retry-btn" onclick="retryLastOperation('${options.containerId}')">
🔄 Try Again
</button>
` : ''}
${options.showAlternatives ? `
<button class="alternatives-btn" onclick="showAlternatives('${error.apiSource}')">
🔍 Browse Popular Options
</button>
` : ''}
<button class="dismiss-btn" onclick="hideError('${options.containerId}')">
✕ Dismiss
</button>
</div>
${options.showDetails ? `
<details class="error-details">
<summary>Technical Details</summary>
<pre>${JSON.stringify({
type: error.type,
code: error.code,
source: error.apiSource,
timestamp: error.timestamp,
retryAttempts: error.retryAttempts || 1
}, null, 2)}</pre>
</details>
` : ''}
`;
}
getErrorIcon(errorType) {
const icons = {
NETWORK_ERROR: '🌐',
AUTH_ERROR: '🔑',
PERMISSION_ERROR: '🚫',
NOT_FOUND: '🔍',
RATE_LIMIT: '⏱️',
SERVER_ERROR: '🔧',
BAD_GATEWAY: '🌉',
SERVICE_UNAVAILABLE: '⚠️',
UNKNOWN_ERROR: '❓'
};
return icons[errorType] || '❗';
}
isCriticalError(error) {
const criticalErrors = ['AUTH_ERROR', 'PERMISSION_ERROR'];
return criticalErrors.includes(error.type);
}
logError(error) {
this.errorHistory.unshift({
...error,
displayTime: new Date().toISOString()
});
if (this.errorHistory.length > this.maxHistorySize) {
this.errorHistory = this.errorHistory.slice(0, this.maxHistorySize);
}
}
hideError(containerId) {
const container = document.getElementById(containerId);
if (container) {
container.innerHTML = '';
container.className = '';
}
}
showErrorHistory() {
console.table(this.errorHistory.slice(0, 10));
return this.errorHistory;
}
}
// Global error display instance
const errorDisplay = new UniversalErrorDisplay();
// Convenience function for displaying errors
function displayUniversalError(containerId, error, options = {}) {
errorDisplay.displayError(containerId, error, {
containerId: containerId,
showAlternatives: true,
showDetails: false,
autoHide: false,
...options
});
}
// Global retry function
function retryLastOperation(containerId) {
console.log(`🔄 Retrying operation for ${containerId}`);
// This would trigger the last failed operation
// Implementation depends on your specific use case
}
// Show alternatives function
function showAlternatives(apiSource) {
const alternatives = fallbackProvider.getPopularAlternatives(apiSource);
console.log(`🔍 Popular ${apiSource} options:`, alternatives);
// Display alternatives in UI
}
Your task:
Try these error scenarios:
Easy: Add error analytics and reporting
Medium: Implement circuit breaker pattern for failing services
Hard: Create intelligent error prediction based on patterns
Outstanding work on building comprehensive error handling! You've learned how to make your API integrations robust and user-friendly even when things go wrong. In the next lesson, we'll explore real-time updates and polling to keep your data fresh and current.
Retries not working?
Fallbacks not showing?
Error display issues?