Error Handling in AJAX
Network Errors vs HTTP Errors
There are two distinct categories of errors in AJAX:
- Network errors — the request never reached the server (no internet, DNS failure, server down, CORS block). With
fetch(), the Promise rejects in this case. - HTTP errors — the server responded, but with an error status (400, 401, 403, 404, 500). With
fetch(), the Promise resolves even for these — you must checkresponse.okmanually.
async function fetchData(url) {
try {
const response = await fetch(url);
// HTTP errors: fetch resolves but response.ok is false
if (!response.ok) {
// Try to read error details from the response body
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
try {
const errorBody = await response.json();
errorMessage = errorBody.message || errorMessage;
} catch {
// response body wasn't JSON — use the status text
}
throw new Error(errorMessage);
}
return await response.json();
} catch (error) {
if (error.name === 'TypeError') {
// Network error — fetch rejected (no internet, CORS, etc.)
console.error('Network error:', error.message);
showUserError('No internet connection. Please try again.');
} else if (error.name === 'AbortError') {
console.warn('Request was cancelled');
} else {
// HTTP error we threw above
console.error('Request failed:', error.message);
showUserError(error.message);
}
return null;
}
}
function showUserError(message) {
const el = document.getElementById('error-banner');
el.textContent = message;
el.style.display = 'block';
}
// Reusable fetch with timeout
async function fetchWithTimeout(url, options = {}, timeoutMs = 8000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { ...options, signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw error;
}
}
// Usage
fetchWithTimeout('/api/data', {}, 5000)
.then(data => console.log(data))
.catch(err => console.error(err.message));
// Retry a fetch up to maxRetries times with exponential backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Don't retry client errors (4xx) — only server errors (5xx) or network issues
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
if (!response.ok) throw new Error(`Server error: ${response.status}`);
return await response.json();
} catch (error) {
lastError = error;
// Don't retry client errors
if (error.message.startsWith('Client error')) throw error;
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 500; // 1s, 2s, 4s
console.warn(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`All ${maxRetries} attempts failed: ${lastError.message}`);
}
// Usage
fetchWithRetry('/api/unstable-endpoint')
.then(data => console.log('Success:', data))
.catch(err => console.error('Gave up:', err.message));
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.