By the end of this lesson, you will:
APIs fail. Networks drop. Servers crash. Users lose internet. Your job as a developer is to ensure your application gracefully handles these inevitable failures.
// ❌ Bad: App crashes with cryptic error
fetch('/api/weather')
.then(response => response.json())
.then(data => {
document.getElementById('temperature').textContent = data.temperature; // Crashes if API fails
});
// ✅ Good: App continues working with helpful feedback
async function fetchWeatherSafely() {
try {
const weather = await weatherAPI.getCurrentWeather();
displayWeather(weather);
} catch (error) {
displayFallbackWeather();
showUserFriendlyError('Weather data temporarily unavailable');
}
}
When the request never reaches the server:
// Common causes: No internet, DNS issues, CORS problems
try {
const response = await fetch('/api/weather');
} catch (error) {
if (error instanceof TypeError) {
console.log('Network error - check internet connection');
}
}
When the server responds with error codes:
async function handleHttpErrors(response) {
if (!response.ok) {
switch (response.status) {
case 400:
throw new Error('Invalid request - check your parameters');
case 401:
throw new Error('Unauthorized - check your API key');
case 403:
throw new Error('Forbidden - insufficient permissions');
case 404:
throw new Error('Resource not found');
case 429:
throw new Error('Rate limit exceeded - please wait');
case 500:
throw new Error('Server error - try again later');
default:
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
}
return response;
}
When the response isn't what you expected:
async function safeJsonParse(response) {
try {
return await response.json();
} catch (error) {
throw new Error('Invalid JSON response from server');
}
}
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`Attempt ${attempt}/${maxRetries}`);
const response = await fetch(url, options);
if (response.ok) {
return response;
}
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
// Retry server errors (5xx) and network issues
lastError = new Error(`Server error: ${response.status}`);
} catch (error) {
lastError = error;
// Don't retry on client errors
if (error.message.includes('Client error')) {
throw error;
}
}
// Wait before retrying (except on last attempt)
if (attempt < maxRetries) {
await sleep(1000 * attempt); // Simple backoff
}
}
throw lastError;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class RetryHandler {
constructor(maxRetries = 3, baseDelay = 1000, maxDelay = 10000) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
this.maxDelay = maxDelay;
}
async executeWithRetry(asyncFunction) {
let lastError;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await asyncFunction();
} catch (error) {
lastError = error;
if (attempt === this.maxRetries || !this.shouldRetry(error)) {
throw error;
}
const delay = this.calculateDelay(attempt);
console.log(`Retry ${attempt}/${this.maxRetries} in ${delay}ms`);
await this.sleep(delay);
}
}
}
shouldRetry(error) {
// Don't retry client errors or certain network errors
return !error.message.includes('Client error') &&
!error.message.includes('Unauthorized');
}
calculateDelay(attempt) {
// Exponential backoff with jitter
const exponentialDelay = this.baseDelay * Math.pow(2, attempt - 1);
const jitter = Math.random() * 0.1 * exponentialDelay;
return Math.min(exponentialDelay + jitter, this.maxDelay);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
class WeatherDashboard {
constructor() {
this.retryHandler = new RetryHandler(3, 1000, 8000);
this.fallbackData = this.loadCachedWeather();
}
async updateWeather(city) {
const loadingElement = document.getElementById('weather-loading');
const errorElement = document.getElementById('weather-error');
try {
this.showLoading(true);
this.hideError();
const weather = await this.retryHandler.executeWithRetry(
() => this.fetchWeatherData(city)
);
this.displayWeather(weather);
this.cacheWeatherData(weather);
} catch (error) {
console.error('Weather fetch failed:', error);
this.handleWeatherError(error);
} finally {
this.showLoading(false);
}
}
handleWeatherError(error) {
if (this.fallbackData) {
this.displayWeather(this.fallbackData, true); // Show as cached
this.showError('Using cached weather data - unable to fetch latest');
} else {
this.showError('Weather data unavailable - please try again later');
this.displayFallbackMessage();
}
}
displayFallbackMessage() {
document.getElementById('weather-display').innerHTML = `
<div class="fallback-message">
<h3>Weather Service Temporarily Unavailable</h3>
<p>Please check your internet connection and try again.</p>
<button onclick="dashboard.updateWeather('${this.lastCity}')">
Retry
</button>
</div>
`;
}
}
Prevent cascading failures by temporarily stopping requests to failing services:
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureThreshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(asyncFunction) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN - service temporarily disabled');
}
this.state = 'HALF_OPEN';
}
try {
const result = await asyncFunction();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
class ErrorMessageHandler {
static getMessageForUser(error) {
const errorMap = {
'Failed to fetch': 'Please check your internet connection',
'Unauthorized': 'Please check your login credentials',
'Rate limit': 'Too many requests - please wait a moment',
'Not found': 'The requested information is not available',
'Server error': 'Our servers are experiencing issues - please try again',
'Timeout': 'Request is taking too long - please try again'
};
for (const [key, message] of Object.entries(errorMap)) {
if (error.message.toLowerCase().includes(key.toLowerCase())) {
return message;
}
}
return 'Something went wrong - please try again';
}
static showErrorToUser(error, containerId = 'error-container') {
const container = document.getElementById(containerId);
const userMessage = this.getMessageForUser(error);
container.innerHTML = `
<div class="error-message">
<div class="error-icon">⚠️</div>
<div class="error-text">${userMessage}</div>
<button class="retry-button" onclick="retryLastAction()">
Try Again
</button>
</div>
`;
container.style.display = 'block';
// Auto-hide after 10 seconds
setTimeout(() => {
container.style.display = 'none';
}, 10000);
}
}
Let's put it all together in a production-ready weather service:
class RobustWeatherService {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.openweathermap.org/data/2.5';
this.retryHandler = new RetryHandler(3, 1000, 8000);
this.circuitBreaker = new CircuitBreaker(5, 30000);
this.cache = new Map();
}
async getCurrentWeather(city) {
const cacheKey = `weather_${city}`;
try {
return await this.circuitBreaker.execute(async () => {
return await this.retryHandler.executeWithRetry(async () => {
const url = `${this.baseUrl}/weather?q=${city}&appid=${this.apiKey}&units=metric`;
const response = await fetch(url);
if (!response.ok) {
await this.handleHttpError(response);
}
const data = await response.json();
// Cache successful response
this.cache.set(cacheKey, {
data,
timestamp: Date.now(),
ttl: 10 * 60 * 1000 // 10 minutes
});
return data;
});
});
} catch (error) {
console.error('Weather service error:', error);
// Try to return cached data
const cached = this.getCachedData(cacheKey);
if (cached) {
console.log('Returning cached weather data');
return { ...cached, fromCache: true };
}
throw error;
}
}
getCachedData(key) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < cached.ttl) {
return cached.data;
}
return null;
}
async handleHttpError(response) {
const error = await response.text();
throw new Error(`HTTP ${response.status}: ${error || response.statusText}`);
}
}
Build your own robust API handler that includes:
class MyRobustAPI {
constructor() {
// Initialize retry handler, circuit breaker, cache
}
async fetchData(endpoint) {
// Implement with error handling and retries
}
}
Add error recovery:
Test different scenarios:
// Test network errors
async function testNetworkError() {
try {
await fetch('http://nonexistent-domain.com/api');
} catch (error) {
console.log('Network error handled:', error.message);
}
}
// Test HTTP errors
async function testHttpError() {
try {
const response = await fetch('https://httpstat.us/500');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
} catch (error) {
console.log('HTTP error handled:', error.message);
}
}
// Test parsing errors
async function testParsingError() {
try {
const response = await fetch('https://httpstat.us/200');
await response.json(); // Will fail if response isn't JSON
} catch (error) {
console.log('Parsing error handled:', error.message);
}
}
In our next lesson, Concept 10: Modern API Architectures, we'll explore:
Your robust error handling skills will be essential as we build more complex, production-ready API integrations!
💡 Pro Tip: Start building error handling into your projects from day one. It's much harder to add comprehensive error handling to existing code than to build it in from the beginning.