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:
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:
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}`);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:
// ❌ 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:
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:
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:
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);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:
// 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:
// 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:
// 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 responseWenn 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.