Skip to main content
โšก Calmops

The Fetch API: A Complete Guide with Async/Await and Error Handling

Introduction

The Fetch API is the modern standard for making HTTP requests in JavaScript. It replaces XMLHttpRequest with a cleaner, Promise-based interface. Combined with async/await, it makes network code readable and maintainable.

Basic GET Request

// Promise chain
fetch('https://api.github.com/users/github')
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error: ${response.status}`);
        }
        return response.json();
    })
    .then(data => console.log(data))
    .catch(error => console.error('Fetch failed:', error));

// async/await (preferred)
async function getUser(username) {
    const response = await fetch(`https://api.github.com/users/${username}`);

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

    return response.json();
}

const user = await getUser('github');
console.log(user.name);

Important: fetch() only rejects on network failure. HTTP errors (404, 500) resolve successfully โ€” you must check response.ok.

POST Request with JSON

async function createUser(userData) {
    const response = await fetch('https://api.example.com/users', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        },
        body: JSON.stringify(userData),
    });

    if (!response.ok) {
        const error = await response.json().catch(() => ({ message: response.statusText }));
        throw new Error(error.message || `HTTP ${response.status}`);
    }

    return response.json();
}

const newUser = await createUser({
    name: 'Alice',
    email: '[email protected]',
});

Request Options Reference

const response = await fetch(url, {
    method: 'POST',          // GET, POST, PUT, PATCH, DELETE
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token123',
        'X-Custom-Header': 'value',
    },
    body: JSON.stringify(data),  // string, FormData, Blob, URLSearchParams
    mode: 'cors',            // cors, no-cors, same-origin
    credentials: 'include',  // omit, same-origin, include
    cache: 'no-cache',       // default, no-store, reload, no-cache, force-cache
    redirect: 'follow',      // follow, error, manual
    signal: controller.signal, // AbortController signal
});

Response Methods

const response = await fetch(url);

// Parse response body (choose one)
const json    = await response.json();    // parse as JSON
const text    = await response.text();    // parse as string
const blob    = await response.blob();    // parse as Blob (files, images)
const buffer  = await response.arrayBuffer(); // parse as ArrayBuffer
const form    = await response.formData(); // parse as FormData

// Response metadata
response.ok          // true if status 200-299
response.status      // HTTP status code (200, 404, 500...)
response.statusText  // "OK", "Not Found", "Internal Server Error"
response.headers     // Headers object
response.url         // final URL (after redirects)
response.redirected  // true if redirected

Error Handling Patterns

Comprehensive Error Handler

class FetchError extends Error {
    constructor(message, status, data) {
        super(message);
        this.name = 'FetchError';
        this.status = status;
        this.data = data;
    }
}

async function apiFetch(url, options = {}) {
    let response;

    try {
        response = await fetch(url, {
            headers: { 'Content-Type': 'application/json', ...options.headers },
            ...options,
        });
    } catch (err) {
        // Network error (no internet, DNS failure, CORS)
        throw new FetchError(`Network error: ${err.message}`, 0, null);
    }

    if (!response.ok) {
        let errorData = null;
        try {
            errorData = await response.json();
        } catch {
            errorData = { message: response.statusText };
        }
        throw new FetchError(
            errorData.message || `HTTP ${response.status}`,
            response.status,
            errorData
        );
    }

    // Handle empty responses (204 No Content)
    if (response.status === 204) return null;

    return response.json();
}

// Usage
try {
    const user = await apiFetch('/api/users/42');
} catch (err) {
    if (err instanceof FetchError) {
        switch (err.status) {
            case 0:   console.error('No internet connection'); break;
            case 401: redirectToLogin(); break;
            case 403: showPermissionError(); break;
            case 404: showNotFound(); break;
            case 422: showValidationErrors(err.data); break;
            default:  console.error('API error:', err.message);
        }
    }
}

Authentication

Bearer Token

const token = localStorage.getItem('authToken');

const response = await fetch('/api/profile', {
    headers: {
        'Authorization': `Bearer ${token}`,
    },
});

Automatic Token Refresh

async function fetchWithAuth(url, options = {}) {
    let token = getAccessToken();

    let response = await fetch(url, {
        ...options,
        headers: { ...options.headers, 'Authorization': `Bearer ${token}` },
    });

    // Token expired โ€” refresh and retry
    if (response.status === 401) {
        token = await refreshAccessToken();
        response = await fetch(url, {
            ...options,
            headers: { ...options.headers, 'Authorization': `Bearer ${token}` },
        });
    }

    return response;
}

Cookies (Cross-Origin)

// Include cookies in cross-origin requests
const response = await fetch('https://api.example.com/data', {
    credentials: 'include',  // sends cookies
});

Timeouts with AbortController

Fetch has no built-in timeout. Use AbortController:

async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal,
        });
        clearTimeout(timeoutId);
        return response;
    } catch (err) {
        clearTimeout(timeoutId);
        if (err.name === 'AbortError') {
            throw new Error(`Request timed out after ${timeoutMs}ms`);
        }
        throw err;
    }
}

// Usage
const response = await fetchWithTimeout('/api/slow-endpoint', {}, 3000);

Cancel a Request

const controller = new AbortController();

// Start request
const fetchPromise = fetch('/api/large-data', { signal: controller.signal });

// Cancel it (e.g., user navigated away)
controller.abort();

try {
    await fetchPromise;
} catch (err) {
    if (err.name === 'AbortError') {
        console.log('Request cancelled');
    }
}

Uploading Files

// Single file
async function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('description', 'My upload');

    const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
        // Don't set Content-Type โ€” browser sets it with boundary
    });

    return response.json();
}

// With progress tracking (XMLHttpRequest needed for upload progress)
// Fetch doesn't support upload progress natively

Parallel Requests

// Fetch multiple resources in parallel
const [users, posts, comments] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json()),
]);

// First to resolve wins
const result = await Promise.race([
    fetch('/api/primary').then(r => r.json()),
    fetch('/api/backup').then(r => r.json()),
]);

// All settle (don't fail on individual errors)
const results = await Promise.allSettled([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
]);

results.forEach((result, i) => {
    if (result.status === 'fulfilled') {
        console.log(`Request ${i} succeeded:`, result.value);
    } else {
        console.error(`Request ${i} failed:`, result.reason);
    }
});

Retry Logic

async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
    for (let attempt = 1; attempt <= retries; attempt++) {
        try {
            const response = await fetch(url, options);

            // Don't retry client errors (4xx)
            if (response.status >= 400 && response.status < 500) {
                return response;
            }

            if (response.ok) return response;

            // Server error โ€” retry
            if (attempt === retries) return response;

        } catch (err) {
            if (attempt === retries) throw err;
        }

        // Exponential backoff
        await new Promise(r => setTimeout(r, delay * attempt));
        console.log(`Retry ${attempt}/${retries} for ${url}`);
    }
}

Building a Simple API Client

class ApiClient {
    constructor(baseURL, defaultHeaders = {}) {
        this.baseURL = baseURL;
        this.defaultHeaders = defaultHeaders;
    }

    async request(path, options = {}) {
        const url = `${this.baseURL}${path}`;
        const response = await fetch(url, {
            ...options,
            headers: { ...this.defaultHeaders, ...options.headers },
        });

        if (!response.ok) {
            const error = await response.json().catch(() => ({}));
            throw Object.assign(new Error(error.message || response.statusText), {
                status: response.status,
                data: error,
            });
        }

        if (response.status === 204) return null;
        return response.json();
    }

    get(path, headers)         { return this.request(path, { method: 'GET', headers }); }
    post(path, body, headers)  { return this.request(path, { method: 'POST',  body: JSON.stringify(body), headers }); }
    put(path, body, headers)   { return this.request(path, { method: 'PUT',   body: JSON.stringify(body), headers }); }
    patch(path, body, headers) { return this.request(path, { method: 'PATCH', body: JSON.stringify(body), headers }); }
    delete(path, headers)      { return this.request(path, { method: 'DELETE', headers }); }
}

// Usage
const api = new ApiClient('https://api.example.com', {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
});

const user = await api.get('/users/42');
const newPost = await api.post('/posts', { title: 'Hello', body: 'World' });
await api.delete('/posts/1');

Resources

Comments