fetch() is nu ingebouwd in elke moderne browser en Node.js 18+. Het verving XMLHttpRequest en elimineerde de noodzaak voor Axios in de meeste projecten. Maar het standaardgebruik dat elke tutorial laat zien — fetch(url).then(r => r.json()) — slaat foutafhandeling over, heeft geen timeout, en bezwijkt in elke echte productieomgeving. Deze gids behandelt de patronen die echt standhouden.

De Basis — GET en POST

fetch() geeft een Promise terug die wordt opgelost met een Response-object. Een GET-verzoek is eenvoudig:

js
const response = await fetch('https://api.example.com/products');
const products = await response.json();

Een POST-verzoek met een JSON-body heeft iets meer instelling nodig:

js
const newProduct = {
  name:     'Wireless Keyboard',
  price:    79.99,
  category: 'electronics'
};

const response = await fetch('https://api.example.com/products', {
  method:  'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${authToken}`
  },
  body: JSON.stringify(newProduct)
});

const created = await response.json();
console.log(`Created product with id: ${created.id}`);
Veelgemaakte fout: vergeten Content-Type: application/json toe te voegen aan POST-verzoeken. Zonder dit zullen veel server-frameworks de body niet verwerken, en krijg je een 400 Bad Request of een lege verzoekbody zonder nuttige foutmelding.

De Tweestapscontrole op Fouten — response.ok

Dit is het belangrijkste patroon om te internaliseren. fetch() verwerpt zijn Promise alleen bij netwerkfouten (geen verbinding, DNS-fout, CORS-blokkering). Een 404-, 401- of 500- respons lost de Promise nog steeds op — met response.ok ingesteld op false. Als je dit niet controleert, geef je stilletjes foutresponsen door aan response.json():

js
// ❌ Broken — 404 and 500 responses appear to succeed
async function getProduct(id) {
  const response = await fetch(`/api/products/${id}`);
  return await response.json(); // parses the error body as if it were data
}

// ✅ Correct — check response.ok before parsing
async function getProduct(id) {
  const response = await fetch(`/api/products/${id}`);

  if (!response.ok) {
    // Try to get the error message from the body if it's JSON
    const errorBody = await response.json().catch(() => null);
    const message   = errorBody?.message ?? `HTTP ${response.status}: ${response.statusText}`;
    throw new Error(message);
  }

  return await response.json();
}

response.ok is true voor statuscodes 200–299. Al het andere — 301-omleidingen (als niet automatisch gevolgd), 400-fouten, 500-fouten — stelt het in op false. Controleer het altijd voordat je gaat verwerken.

AbortController — Timeouts en Annulering

fetch() heeft geen ingebouwde timeout. Een verzoek kan voor onbepaalde tijd hangen als de server stopt met reageren tijdens een overdracht. De oplossing is AbortController:

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

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });

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

    return await response.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw err;
  } finally {
    clearTimeout(timeoutId);
  }
}

// Usage
try {
  const data = await fetchWithTimeout('/api/products', {}, 5000);
} catch (err) {
  console.error(err.message); // "Request timed out after 5000ms"
}

AbortController is ook handig voor het annuleren van actieve verzoeken wanneer een gebruiker wegnavigateert of een nieuwe zoekopdracht uitvoert voordat de vorige is voltooid:

js
let searchController = null;

async function searchProducts(query) {
  // Cancel any previous search request
  if (searchController) {
    searchController.abort();
  }

  searchController = new AbortController();

  try {
    const response = await fetch(
      `/api/products/search?q=${encodeURIComponent(query)}`,
      { signal: searchController.signal }
    );
    return await response.json();
  } catch (err) {
    if (err.name !== 'AbortError') throw err;
    return null; // request was cancelled — that's OK
  }
}

Retry met Exponentiële Backoff

Netwerkverzoeken mislukken tijdelijk — een 503 bij één poging slaagt vaak bij de volgende. Exponentiële backoff is de standaardstrategie: progressief langer wachten tussen pogingen om te voorkomen dat een overbelaste server wordt overspoeld:

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

      // Don't retry client errors (4xx) — they won't fix themselves
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error ${response.status} — not retrying`);
      }

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

      return await response.json();

    } catch (err) {
      const isLastAttempt = attempt === retries;

      if (isLastAttempt || err.message.includes('Client error')) {
        throw err;
      }

      // Exponential backoff with jitter: 500ms, 1000ms, 2000ms + random
      const delay = baseDelayMs * 2 ** (attempt - 1) + Math.random() * 100;
      console.warn(`Attempt ${attempt} failed, retrying in ${Math.round(delay)}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Usage
const products = await fetchWithRetry('/api/products', {}, 3, 500);
De jitter doet ertoe: Het toevoegen van Math.random() * 100 aan de vertraging voorkomt "thundering herd" — waarbij duizenden clients allemaal opnieuw proberen op precies hetzelfde moment na een serverstoring. Een kleine willekeurige offset, een groot betrouwbaarheidsvoordeel.

Een Interceptorpatroon — fetch() Omhullen

Axios maakte het interceptorconcept populair: een hook die wordt uitgevoerd vóór elk verzoek en na elke respons. Je kunt hetzelfde bouwen als een dunne wrapper rondom fetch():

js
// api.js — your project's fetch wrapper
const API_BASE = 'https://api.example.com';

function getAuthToken() {
  return localStorage.getItem('authToken');
}

async function apiFetch(path, options = {}) {
  const url = `${API_BASE}${path}`;

  // Request interceptor — add auth header to every request
  const headers = {
    'Content-Type': 'application/json',
    ...options.headers
  };

  const token = getAuthToken();
  if (token) headers['Authorization'] = `Bearer ${token}`;

  const response = await fetch(url, { ...options, headers });

  // Response interceptor — handle auth expiry globally
  if (response.status === 401) {
    logout(); // token expired — redirect to login
    throw new Error('Session expired');
  }

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

  // Return null for 204 No Content
  if (response.status === 204) return null;

  return response.json();
}

// Usage — clean, no repeated boilerplate
const products = await apiFetch('/products');
const created  = await apiFetch('/products', {
  method: 'POST',
  body:   JSON.stringify({ name: 'New Product', price: 29.99 })
});

Dit patroon centraliseert authenticatie, foutafhandeling en basis-URL-configuratie. Elk API-aanroep in je codebase krijgt gratis hetzelfde gedrag. Wanneer vereisten veranderen — zoals het toevoegen van een nieuwe header of overstappen van JWT naar sessiecookies — update je één plek.

Omgaan met Verschillende Responstypen

Niet elke API geeft JSON terug. fetch() heeft methoden voor alle veelvoorkomende responstypen:

js
// JSON (most common)
const data = await response.json();

// Plain text (CSV, HTML, logs)
const csvText = await response.text();

// Binary data — download a file
const blob       = await response.blob();
const blobUrl    = URL.createObjectURL(blob);
const downloadLink = document.createElement('a');
downloadLink.href     = blobUrl;
downloadLink.download = 'export.csv';
downloadLink.click();
URL.revokeObjectURL(blobUrl); // clean up

// ArrayBuffer — for WebAssembly or typed arrays
const buffer     = await response.arrayBuffer();
const byteArray  = new Uint8Array(buffer);

// Form data (multipart responses)
const formData = await response.formData();

CORS — Wat Ontwikkelaars Moeten Weten

CORS (Cross-Origin Resource Sharing) wordt afgedwongen door de browser, niet de server. Wanneer je frontend op app.example.com ophaalt van api.example.com, stuurt de browser een Origin-header en controleert de respons op toestemming. Dit is wat je van de clientzijde kunt instellen:

js
// credentials: 'include' — send cookies/session tokens cross-origin
// The server must also respond with Access-Control-Allow-Credentials: true
const response = await fetch('https://api.example.com/account', {
  credentials: 'include'
});

// credentials: 'same-origin' — default, only sends credentials to same origin
// credentials: 'omit' — never send credentials (useful for public CDN requests)

// mode: 'no-cors' — fire-and-forget for cross-origin requests
// You get a "opaque" response — no status, no body, no error
await fetch('https://analytics.example.com/event', {
  method: 'POST',
  mode:   'no-cors',
  body:   JSON.stringify({ event: 'page_view' })
});
// Use for analytics pings where you don't need the response

Als je CORS-fouten krijgt, ligt de oplossing bijna altijd op de server: de server moet Access-Control-Allow-Origin toevoegen aan zijn responsheaders. De CORS-gids van MDN is de meest uitgebreide referentie voor hoe je dit configureert.

Handige Tools

Wanneer je fetch-responsen debugt, maakt JSON Formatter API-payloads leesbaar en JSON Validator vangt misvormde responsen. Voor het veilig coderen van queryparameters heeft JSON URL Encode je gedekt. De MDN Using Fetch-gids is de beste enkele referentie voor alle fetch-opties en responsmethoden.

Afsluiting

fetch() is een solide basis voor HTTP-verzoeken in JavaScript, maar de standaardinstellingen laten alles weg wat je in productie nodig hebt. Controleer altijd response.ok voordat je gaat verwerken. Voeg timeouts toe met AbortController. Bouw een dunne wrapperfunctie om auth-headers, foutnormalisatie en 401-omleidingen op één plek te verwerken. Voeg retry-logica toe met exponentiële backoff voor verzoeken die tijdelijk kunnen mislukken. Die vier gewoonten zijn het verschil tussen democode en productiecode.