Skip to content
Home
JavaScript Fetch API: Complete Guide

JavaScript Fetch API: Complete Guide

JavaScript JavaScript 8 min read 1534 words Beginner ExcellentWiki Editorial Team

The Fetch API is the modern replacement for XMLHttpRequest. It provides a promise-based interface for making HTTP requests in both browsers and Node.js (since v18). This guide covers the full API surface and real-world patterns.

Basic GET Request

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

The fetch function returns a Promise that resolves to a Response object. The Promise resolves immediately after receiving the response headers — the body may still be streaming. Always check response.ok (status in 200-299 range) or inspect response.status directly.

Async/Await Version

async function getUsers() {
    try {
        const response = await fetch('https://api.example.com/users');
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Failed to fetch users:', error);
        throw error; // re-throw if caller needs to handle
    }
---

POST Request

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

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

    return response.json();
---

Request Options

const response = await fetch(url, {
    method: 'GET',                   // GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
    },
    body: JSON.stringify(data),      // for POST/PUT/PATCH
    mode: 'cors',                    // cors, no-cors, same-origin, navigate
    credentials: 'include',          // omit, same-origin, include
    cache: 'default',                // default, no-cache, reload, force-cache, only-if-cached
    redirect: 'follow',              // follow, error, manual
    referrerPolicy: 'strict-origin-when-cross-origin',
    signal: AbortSignal.timeout(5000), // timeout
---);

Response Methods

const response = await fetch(url);

// Read the response body
const json = await response.json();
const text = await response.text();
const blob = await response.blob();       // binary data (images, files)
const formData = await response.formData();
const arrayBuffer = await response.arrayBuffer();

// Response metadata
console.log(response.status);       // 200
console.log(response.statusText);   // 'OK'
console.log(response.ok);           // true (status 200-299)
console.log(response.headers);      // Headers object
console.log(response.url);          // final URL (after redirects)
console.log(response.type);         // basic, cors, error, opaque
console.log(response.redirected);   // true if followed redirects

The response body can only be read once. Calling response.json() after response.text() will throw — the body is a readable stream that gets consumed. Clone the response first if you need multiple reads: response.clone().json().

Headers

// Creating headers
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.append('X-Custom', 'value1');
headers.append('X-Custom', 'value2'); // multiple values for same key

// Headers from init object
const headers = new Headers({
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123',
---);

// Reading headers
const response = await fetch(url);
console.log(response.headers.get('Content-Type'));
console.log(response.headers.has('X-RateLimit-Remaining'));

// Iterating headers
for (const [key, value] of response.headers) {
    console.log(key, value);
---

// Setting cache control
headers.set('Cache-Control', 'no-cache');

Error Handling

async function fetchWithRetry(url, options = {}, retries = 3) {
    for (let attempt = 1; attempt <= retries; attempt++) {
        try {
            const response = await fetch(url, {
                ...options,
                signal: AbortSignal.timeout(10000),
            });

            if (!response.ok) {
                throw new HTTPError(response.status, await response.text());
            }

            return await response.json();
        } catch (error) {
            if (attempt === retries) throw error;

            // Don't retry non-retryable errors
            if (error.name === 'AbortError') throw error;
            if (error instanceof HTTPError && error.status < 500) throw error;

            console.warn(`Attempt ${attempt} failed, retrying...`);
            await new Promise(r => setTimeout(r, 1000 * attempt)); // exponential backoff
        }
    }
---

Not all errors should be retried. 4xx errors (client errors) should not be retried — they will fail the same way. 5xx errors, network failures, and timeouts can benefit from retry with backoff.

File Upload

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

    const response = await fetch('/upload', {
        method: 'POST',
        body: formData, // Content-Type automatically set to multipart/form-data
    });
    return response.json();
---

// Multiple files
async function uploadMultiple(files) {
    const formData = new FormData();
    files.forEach((file, index) => {
        formData.append(`file_${index}`, file);
    });

    const response = await fetch('/upload-multiple', {
        method: 'POST',
        body: formData,
    });
    return response.json();
---

// Upload progress (requires XMLHttpRequest or custom solution)
// Fetch does not natively support upload progress tracking.

Aborting Requests

// Method 1: AbortController
const controller = new AbortController();
const { signal } = controller;

// Start the request
const promise = fetch(url, { signal });

// Cancel it (e.g., user clicked cancel)
controller.abort();

// Method 2: Timeout (built-in)
const response = await fetch(url, {
    signal: AbortSignal.timeout(5000)
---);

// Method 3: Timeout with AbortController
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return response.json();
--- catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
        console.log('Request was aborted (timeout or user cancellation)');
    }
    throw error;
---

Streaming Responses

For large payloads, stream the body instead of waiting for the full download:

async function streamResponse(url) {
    const response = await fetch(url);
    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value, { stream: true });
        processChunk(chunk);
        console.log('Received chunk:', chunk.length, 'bytes');
    }
---

// JSON streaming (newline-delimited JSON)
async function streamJSON(url) {
    const response = await fetch(url);
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop(); // keep incomplete line

        for (const line of lines) {
            if (line.trim()) {
                yield JSON.parse(line);
            }
        }
    }
---

API Versioning Strategies

Version your API to maintain backward compatibility:

// URL-based versioning
mux.HandleFunc("GET /v1/api/items", v1Handler)
mux.HandleFunc("GET /v2/api/items", v2Handler)

// Header-based versioning
func versionMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        version := r.Header.Get("Accept-Version")
        ctx := context.WithValue(r.Context(), "version", version)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
---

Rate Limiting

type RateLimiter struct {
    mu       sync.Mutex
    visitors map[string]*visitor
    rate     int
    burst    int
---

type visitor struct {
    limiter  *rate.Limiter
    lastSeen time.Time
---

func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := r.RemoteAddr
        rl.mu.Lock()
        v, exists := rl.visitors[ip]
        if !exists {
            v = &visitor{limiter: rate.NewLimiter(rate.Limit(rl.rate), rl.burst)}
            rl.visitors[ip] = v
        }
        v.lastSeen = time.Now()
        rl.mu.Unlock()

        if !v.limiter.Allow() {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
---

Real-World Implementation Tips

Production Considerations

When moving from development to production, several factors become critical. Error handling should be comprehensive — every external call (database, API, file system) should have proper error checking, logging, and retry logic where appropriate. Performance monitoring through metrics and structured logging helps identify bottlenecks before they affect users.

Testing Strategy

A thorough testing approach combines multiple levels:

  • Unit tests verify individual functions and methods in isolation
  • Integration tests validate that components work together correctly
  • Edge case tests cover boundary conditions, empty inputs, and error states
  • Performance tests ensure the system meets latency and throughput requirements

Test data should be realistic but controlled. Mock external dependencies to make tests fast and deterministic. Aim for tests that are independent, repeatable, and fast enough to run on every commit.

Documentation

Good documentation is essential for maintainable code. Follow these principles:

  • Document the “why” not just the “what” — explain design decisions
  • Keep examples up to date with the code
  • Include usage examples for public APIs
  • Document configuration options and their defaults
  • Explain error conditions and recovery strategies

Security Best Practices

Security should be considered throughout development:

  • Validate all inputs at system boundaries
  • Use parameterized queries for database access
  • Store secrets in environment variables or secret managers
  • Keep dependencies updated to patch vulnerabilities
  • Apply the principle of least privilege

Performance Optimization

Optimize based on measured data, not assumptions:

  1. Profile before optimizing — identify actual bottlenecks
  2. Measure the impact of each change
  3. Consider the trade-off between speed and readability
  4. Cache expensive operations with appropriate invalidation
  5. Use connection pooling for database and network resources

Monitoring and Observability

Production systems need visibility:

  • Structured logging with correlation IDs for request tracking
  • Metrics for latency, throughput, error rates, and resource usage
  • Health check endpoints for load balancers and orchestration
  • Distributed tracing for request flows across services
  • Alerts for anomaly detection based on baselines

These patterns apply across all programming languages and frameworks. The specific implementation varies, but the principles remain consistent.

FAQ

Q: What is the difference between fetch and XMLHttpRequest? A: Fetch is promise-based, cleaner syntax, supports streaming, and integrates with Service Workers. XHR supports upload progress tracking, synchronous requests (deprecated), and broader browser support.

Q: Does fetch follow redirects automatically? A: Yes, by default. Set redirect: 'manual' to handle redirects yourself.

Q: How do I send cookies with fetch? A: Set credentials: 'include' to send cookies cross-origin, or credentials: 'same-origin' for same-origin requests.

Q: How do I set a request timeout? A: Use AbortSignal.timeout(ms) as the signal option: fetch(url, { signal: AbortSignal.timeout(5000) }).

Q: How do I cancel a fetch request? A: Use an AbortController. Call controller.abort() to cancel the request, which rejects the fetch promise with an AbortError.

Q: Why does fetch not reject on HTTP error status? A: By design — fetch only rejects on network failures. Check response.ok or response.status to handle HTTP errors.

Q: Can I use fetch in Node.js? A: Yes, fetch is built into Node.js since v18 (experimental in v17.5+). No extra dependencies needed.

For a comprehensive overview, read our article on Javascript Arrays Guide.

For a comprehensive overview, read our article on Javascript Async Await.

Section: JavaScript 1534 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top