Understanding the Fetch API: Modern AJAX in JavaScript

A comprehensive guide to making HTTP requests in the browser

Introduction to fetch

Think of fetch as your browser's delivery service. Just like how you might use a delivery app to order food, fetch lets you request data from servers. Here's the basic syntax:


// Basic fetch syntax
fetch(url, options)

// Simple GET request
fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error));

// Async/await version
async function getData() {
    try {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('Error:', error);
    }
}

Making Different Types of Requests

1. GET Request (Retrieving Data)


// Fetching user data
async function getUser(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const user = await response.json();
        return user;
    } catch (error) {
        console.error('Failed to fetch user:', error);
        throw error;
    }
}

// With query parameters
async function searchProducts(query, category, page = 1) {
    const params = new URLSearchParams({
        q: query,
        category: category,
        page: page
    });
    
    try {
        const response = await fetch(`/api/products/search?${params}`);
        return await response.json();
    } catch (error) {
        console.error('Search failed:', error);
        return [];
    }
}

2. POST Request (Creating Data)


// Creating a new user
async function createUser(userData) {
    try {
        const response = await fetch('/api/users', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(userData)
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        return await response.json();
    } catch (error) {
        console.error('Failed to create user:', error);
        throw error;
    }
}

// Submitting a form
async function submitForm(formData) {
    try {
        const response = await fetch('/api/submit', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: new URLSearchParams(formData)
        });
        
        return await response.json();
    } catch (error) {
        console.error('Form submission failed:', error);
        throw error;
    }
}

Working with Response Objects

The fetch API provides various methods to handle different types of responses:


async function handleMultipleResponseTypes() {
    try {
        const response = await fetch('/api/data');
        
        // Check response type
        const contentType = response.headers.get('content-type');
        
        if (contentType?.includes('application/json')) {
            return await response.json();
        } else if (contentType?.includes('text/plain')) {
            return await response.text();
        } else if (contentType?.includes('application/pdf')) {
            return await response.blob();
        } else {
            throw new Error(`Unsupported content type: ${contentType}`);
        }
    } catch (error) {
        console.error('Request failed:', error);
        throw error;
    }
}

// Handling response headers
async function checkRateLimits() {
    const response = await fetch('/api/data');
    
    console.log({
        remainingCalls: response.headers.get('X-RateLimit-Remaining'),
        resetTime: response.headers.get('X-RateLimit-Reset')
    });
}

Advanced Fetch Patterns

1. Request Timeout


async function fetchWithTimeout(url, timeout = 5000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    
    try {
        const response = await fetch(url, {
            signal: controller.signal
        });
        
        clearTimeout(timeoutId);
        return await response.json();
    } catch (error) {
        clearTimeout(timeoutId);
        if (error.name === 'AbortError') {
            throw new Error('Request timed out');
        }
        throw error;
    }
}

// Usage
try {
    const data = await fetchWithTimeout('/api/data', 3000);
    console.log('Data received:', data);
} catch (error) {
    console.error('Request failed:', error);
}

2. Retry Logic


async function fetchWithRetry(url, options = {}, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
        try {
            const response = await fetch(url, options);
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            return await response.json();
        } catch (error) {
            const isLastAttempt = i === maxRetries - 1;
            if (isLastAttempt) throw error;
            
            // Wait longer between each retry
            const delay = Math.min(1000 * Math.pow(2, i), 10000);
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

3. Request Queue


class RequestQueue {
    constructor(concurrency = 3) {
        this.concurrency = concurrency;
        this.queue = [];
        this.active = 0;
    }
    
    async add(request) {
        if (this.active >= this.concurrency) {
            // Wait for a slot to open
            await new Promise(resolve => this.queue.push(resolve));
        }
        
        this.active++;
        
        try {
            return await request();
        } finally {
            this.active--;
            if (this.queue.length > 0) {
                // Let the next request proceed
                this.queue.shift()();
            }
        }
    }
}

// Usage
const queue = new RequestQueue(2);

async function processUrls(urls) {
    const results = [];
    
    for (const url of urls) {
        const result = await queue.add(async () => {
            const response = await fetch(url);
            return response.json();
        });
        results.push(result);
    }
    
    return results;
}

Error Handling Best Practices


// Comprehensive error handler
async function safeFetch(url, options = {}) {
    try {
        const response = await fetch(url, {
            ...options,
            headers: {
                'Content-Type': 'application/json',
                ...options.headers
            }
        });

        // Handle different types of errors
        if (response.status === 404) {
            throw new Error('Resource not found');
        }
        
        if (response.status === 401) {
            // Handle authentication error
            window.location.href = '/login';
            throw new Error('Authentication required');
        }
        
        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.message || 'Request failed');
        }

        return await response.json();
    } catch (error) {
        // Log error for debugging
        console.error('Request failed:', {
            url,
            error: error.message,
            timestamp: new Date().toISOString()
        });
        
        // Rethrow with user-friendly message
        throw new Error('Unable to complete request. Please try again later.');
    }
}

// Usage with different scenarios
async function handleUserData() {
    try {
        // GET request
        const userData = await safeFetch('/api/user/profile');
        
        // POST request
        await safeFetch('/api/user/settings', {
            method: 'POST',
            body: JSON.stringify({ theme: 'dark' })
        });
        
        // Update UI
        updateUserInterface(userData);
    } catch (error) {
        // Show user-friendly error message
        showErrorNotification(error.message);
    }
}