fetch() ist mittlerweile in jedem modernen Browser und Node.js 18+ integriert. Es hat XMLHttpRequest abgelöst und macht Axios in den meisten Projekten überflüssig. Aber die Standard-Verwendung, die jedes Tutorial zeigt — fetch(url).then(r => r.json()) — überspringt die Fehlerbehandlung, hat kein Timeout und versagt in jeder echten Produktionsumgebung. Dieser Leitfaden behandelt die Muster, die wirklich standhalten.

Die Grundlagen — GET und POST

fetch() gibt ein Promise zurück, das mit einem Response-Objekt aufgelöst wird. Eine GET-Anfrage ist einfach:

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

Eine POST-Anfrage mit einem JSON-Body benötigt etwas mehr Konfiguration:

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}`);
Häufiger Fehler: Das Vergessen von Content-Type: application/json bei POST-Anfragen. Ohne es werden viele Server-Frameworks den Body nicht parsen, und Sie erhalten einen 400 Bad Request oder einen leeren Anfrage-Body ohne nützliche Fehlermeldung.

Die Zweistufige Fehlerprüfung — response.ok

Dies ist das wichtigste Muster zum Verinnerlichen. fetch() lehnt sein Promise nur bei Netzwerkfehlern ab (keine Verbindung, DNS-Fehler, CORS-Blockierung). Eine 404-, 401- oder 500-Antwort löst das Promise trotzdem auf — mit response.ok auf false gesetzt. Wenn Sie das nicht prüfen, werden Fehlerantworten stillschweigend an response.json() weitergegeben:

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 ist true für Statuscodes 200–299. Alles andere — 301-Weiterleitungen (wenn nicht automatisch gefolgt), 400-Fehler, 500-Fehler — setzt es auf false. Prüfen Sie es immer, bevor Sie parsen.

AbortController — Timeouts und Abbruch

fetch() hat kein eingebautes Timeout. Eine Anfrage kann unbegrenzt hängen, wenn der Server mitten in einer Übertragung aufhört zu antworten. Die Lösung ist der 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 ist auch nützlich, um laufende Anfragen abzubrechen, wenn ein Benutzer die Seite verlässt oder eine neue Suche startet, bevor die vorherige abgeschlossen ist:

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 mit Exponentiellem Backoff

Netzwerkanfragen scheitern vorübergehend — ein 503 bei einem Versuch gelingt oft beim nächsten. Exponentieller Backoff ist die Standardstrategie: progressiv länger zwischen Versuchen warten, um einen überlasteten Server nicht zu überfluten:

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);
Der Jitter-Anteil ist wichtig: Das Hinzufügen von Math.random() * 100 zur Verzögerung verhindert das "Thundering Herd"-Problem — bei dem Tausende von Clients nach einem Serverausfall alle genau zum gleichen Zeitpunkt einen Retry durchführen. Kleiner zufälliger Versatz, großer Zuverlässigkeitsgewinn.

Ein Interceptor-Muster — fetch() Einpacken

Axios hat das Interceptor-Konzept popularisiert: ein Hook, der vor jeder Anfrage und nach jeder Antwort ausgeführt wird. Sie können dasselbe als schlanken Wrapper um fetch() bauen:

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 })
});

Dieses Muster zentralisiert Authentifizierung, Fehlerbehandlung und Basis-URL-Konfiguration. Jeder API-Aufruf in Ihrer Codebasis erhält automatisch dasselbe Verhalten. Wenn sich Anforderungen ändern — wie das Hinzufügen eines neuen Headers oder der Wechsel von JWT zu Session-Cookies — aktualisieren Sie nur eine Stelle.

Verschiedene Antworttypen Verarbeiten

Nicht jede API gibt JSON zurück. fetch() hat Methoden für alle gängigen Antworttypen:

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 — Was Entwickler Wissen Müssen

CORS (Cross-Origin Resource Sharing) wird vom Browser erzwungen, nicht vom Server. Wenn Ihr Frontend unter app.example.com Daten von api.example.com abruft, sendet der Browser einen Origin-Header und prüft die Antwort auf Berechtigung. Folgendes können Sie auf der Client-Seite steuern:

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

Wenn Sie CORS-Fehler erhalten, liegt die Lösung fast immer auf dem Server: Der Server muss Access-Control-Allow-Origin zu seinen Antwort-Headern hinzufügen. MDNs CORS-Leitfaden ist die umfassendste Referenz für die Konfiguration.

Nützliche Werkzeuge

Beim Debuggen von fetch-Antworten macht der JSON-Formatierer API-Payloads lesbar, und der JSON-Validator erkennt fehlerhafte Antworten. Zum sicheren Kodieren von Query-Parametern ist JSON URL Encode die richtige Wahl. Der MDN-Leitfaden zu Using Fetch ist die beste Einzelreferenz für alle fetch-Optionen und Antwortmethoden.

Zusammenfassung

fetch() ist eine solide Grundlage für HTTP-Anfragen in JavaScript, aber die Standardeinstellungen lassen alles weg, was Sie in der Produktion brauchen. Überprüfen Sie immer response.ok vor dem Parsen. Fügen Sie Timeouts mit AbortController hinzu. Bauen Sie eine schlanke Wrapper-Funktion, um Auth-Header, Fehlernormalisierung und 401-Weiterleitungen an einer Stelle zu behandeln. Fügen Sie Retry-Logik mit exponentiellem Backoff für Anfragen hinzu, die vorübergehend fehlschlagen können. Diese vier Gewohnheiten machen den Unterschied zwischen Demo-Code und Produktionscode.