JavaScript Fetch API & AJAX Cheat Sheet
Covers making GET and POST requests with the Fetch API, handling errors, timeouts, and JSON, and the legacy XMLHttpRequest API.
Basic GET Request
fetch() returns a Promise that resolves to a Response.
fetch("/api/users") .then(response => response.json()) .then(data => console.log(data)) .catch(err => console.error("Network error:", err));// With async/awaitasync function getUsers() { const response = await fetch("/api/users"); if (!response.ok) { throw new Error(`HTTP error ${response.status}`); } return response.json();}
POST with a JSON Body
Set headers and stringify the payload.
async function createUser(user) { const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify(user), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json();}createUser({ name: "Alice", email: "[email protected]" });
Error Handling & Timeouts
fetch only rejects on network failure, not HTTP errors.
async function fetchWithTimeout(url, ms = 5000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), ms); try { const response = await fetch(url, { signal: controller.signal }); if (!response.ok) { throw new Error(`Server responded ${response.status}`); } return await response.json(); } catch (err) { if (err.name === "AbortError") console.error("Request timed out"); else console.error("Fetch failed:", err.message); throw err; } finally { clearTimeout(timer); }}// Note: fetch() only rejects on network failure -- a 404/500 still resolves with ok: false
Legacy XMLHttpRequest
Still useful for upload progress events fetch lacks by default.
const xhr = new XMLHttpRequest();xhr.open("GET", "/api/users");xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { console.log(JSON.parse(xhr.responseText)); }};xhr.onerror = () => console.error("Request failed");xhr.send();// XHR still has native upload-progress events fetch lacks by default:xhr.upload.onprogress = (e) => console.log(e.loaded / e.total);
fetch() Options Reference
Common fields in the fetch init object.
- method- HTTP verb: 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'
- headers- Object or Headers instance for request headers
- body- Request payload: string, FormData, Blob, or URLSearchParams
- credentials- 'omit' | 'same-origin' | 'include' -- whether to send cookies cross-origin
- mode- 'cors' | 'no-cors' | 'same-origin' -- cross-origin request behavior
- signal- AbortSignal used with AbortController to cancel the request
- response.ok- true for status codes 200-299; fetch does NOT reject on HTTP error statuses
Streaming a Response Body
response.body is a ReadableStream you can read incrementally instead of buffering the whole payload.
async function streamText(url, onChunk) { 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; onChunk(decoder.decode(value, { stream: true })); }}// Useful for large downloads, progress bars, or consuming an SSE-like stream// without waiting for response.json()/response.text() to buffer everything.
File Upload with FormData
FormData builds a multipart/form-data body; never set Content-Type manually with it.
async function uploadFile(file, extraFields = {}) { const form = new FormData(); form.append("file", file, file.name); for (const [key, value] of Object.entries(extraFields)) { form.append(key, value); } const response = await fetch("/api/upload", { method: "POST", body: form, // Do NOT set 'Content-Type' -- the browser adds the multipart boundary itself }); if (!response.ok) throw new Error(`Upload failed: ${response.status}`); return response.json();}
Retry with Exponential Backoff & Jitter
Retry transient failures without hammering a struggling server.
async function fetchWithRetry(url, options = {}, maxRetries = 3) { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const response = await fetch(url, options); if (response.status >= 500 && attempt < maxRetries) { throw new Error(`Server error ${response.status}`); } return response; } catch (err) { if (attempt === maxRetries) throw err; const base = 2 ** attempt * 200; const jitter = Math.random() * 100; await new Promise(r => setTimeout(r, base + jitter)); } }}
Cloning Requests/Responses & Cache Control
A body stream can only be read once -- clone() lets you read it twice.
async function logAndParse(url) { const response = await fetch(url, { cache: "no-store" }); const copy = response.clone(); // Duplicate the stream before consuming it copy.text().then(raw => console.log("raw body:", raw)); // Independent read return response.json(); // Original read}// cache modes: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached'fetch("/api/data", { cache: "force-cache" }); // Prefer HTTP cache, even if stalefetch("/api/data", { cache: "reload" }); // Bypass cache, but store the new response
Advanced Fetch Concepts
Behaviors that trip people up beyond basic GET/POST.
- keepalive: true- Lets a fetch survive page unload (analytics/beacon-style requests), with a small body-size limit
- CORS preflight- A non-simple request (custom headers, non-GET/POST/HEAD, or JSON content-type) triggers an automatic OPTIONS request first
- Headers iteration- 'for (const [k, v] of response.headers)' iterates entries; header names are case-insensitive and normalized to lowercase
- Response.redirected- Boolean flag telling you whether the final response came after at least one redirect
- redirect: 'manual'- Returns an opaque-redirect response instead of following it automatically
- AbortSignal.timeout(ms)- Shorthand that creates a signal which auto-aborts after ms, without manually wiring an AbortController + setTimeout
- duplex: 'half'- Required option (in supporting environments) when body is a ReadableStream, since streaming request bodies is still an evolving spec area
fetch() only rejects its promise on a network-level failure (DNS, CORS, offline) -- a 404 or 500 response still resolves successfully, so always check response.ok explicitly instead of relying on .catch() alone.