The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Fetch JSON | const response = await fetch('/api/items', { headers: { Accept: 'application/json' } }) | View examples |
| Reject an HTTP error | if (!response.ok) throw new Error(`HTTP ${response.status}`) | View examples |
| Consume JSON once | const data = await response.json() | View examples |
| Construct a Request | const request = new Request('/api/items', { method: 'GET', cache: 'no-cache' }) | View examples |
| Require a CORS response | await fetch('https://api.example/data', { mode: 'cors' }) | View examples |
| Include cross-origin credentials | await fetch(url, { mode: 'cors', credentials: 'include' }) | View examples |
| Enforce same-origin access | await fetch(url, { mode: 'same-origin' }) | View examples |
| Cancel with a controller | const controller = new AbortController(); fetch(url, { signal: controller.signal }) | View examples |
| Apply a deadline | await fetch(url, { signal: AbortSignal.timeout(5000) }) | View examples |
| Combine cancellation sources | const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]) | View examples |
| Acquire a byte reader | const reader = response.body.getReader() | View examples |
| Decode streamed text | const textStream = response.body.pipeThrough(new TextDecoderStream()) | View examples |
| Pipe with cancellation | await response.body.pipeTo(destination, { signal }) | View examples |
| Check body disturbance | if (response.bodyUsed) throw new Error('Body already consumed') | View examples |
| Clone before reading | const auditCopy = response.clone() | View examples |
| Upload a stream | await fetch('/upload', { method: 'POST', body: stream, duplex: 'half' }) | View examples |
Fetch separates response metadata from a potentially long-lived body stream. Its promise normally resolves for HTTP errors and can resolve before the body finishes, so status checks, decoding, cancellation, and stream failures need their own handling. Cross-origin access remains controlled by the server's CORS response, credentials add stricter rules, and every request or response body is a one-use stream. Robust clients model those boundaries explicitly and let stream backpressure regulate memory instead of buffering unbounded data.
Step by step
Detailed examples
Handle response metadata and body consumption as separate phases
fetch resolves with a Response after response metadata becomes available; bytes may still be arriving. HTTP 404 or 500 is a successful fetch at the protocol layer, so check ok or status before deciding what the application accepts. Network, policy, and CORS failures reject, usually with TypeError, but useful security details are intentionally hidden. json, text, blob, bytes, and arrayBuffer consume the body and can reject later; JSON parsing can also throw SyntaxError. Keep status validation and body decoding in one deliberate error boundary, and validate decoded data before trusting its shape.
async function loadItems(signal) {
const response = await fetch('/api/items', {
signal,
headers: { Accept: 'application/json' }
});
if (!response.ok) {
throw new Error(`Items request failed: HTTP ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data.items)) throw new TypeError('Invalid items payload');
return data.items;
} Treat CORS as a server authorization protocol, not a client switch
cors is the normal mode for script-readable cross-origin responses. Non-safelisted methods, headers, content types such as application/json, and request streams can trigger a credential-free OPTIONS preflight. no-cors is not a workaround: it restricts the request and yields an opaque response with status 0, inaccessible headers, and no readable body. credentials defaults to same-origin. Cross-origin include requires the server to return the exact allowed origin and Access-Control-Allow-Credentials: true; wildcard origin is invalid. Allowlist reflected origins, send Vary: Origin for dynamic responses, and remember that SameSite and third-party-cookie policies still apply. CORS prevents reading responses; it is not CSRF protection.
const response = await fetch('https://api.example.test/profile', {
mode: 'cors',
credentials: 'include',
headers: { Accept: 'application/json' }
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const profile = await response.json();
// The server response must include:
// Access-Control-Allow-Origin: https://app.example.test
// Access-Control-Allow-Credentials: true
// Vary: Origin Carry cancellation through headers, streaming, and cleanup
Fetch has no timeout option. AbortSignal.timeout provides a deadline in supporting browsers, AbortController exposes caller cancellation, and AbortSignal.any combines sources. Feature-detect the static conveniences for older targets and fall back to a controller plus a cleared timer. The same signal covers the request and subsequent body read: aborting before headers rejects fetch, while aborting after resolution errors the body. Keep body consumption inside the try block. A timeout signal generally rejects with TimeoutError and an ordinary controller without a custom reason uses AbortError, but signal.reason is the authoritative reason. Do not automatically retry unsafe methods or one-shot bodies unless the operation is idempotent or has an application idempotency key.
async function loadReport(url, callerSignal) {
const deadline = AbortSignal.timeout(8000);
const signal = AbortSignal.any([callerSignal, deadline]);
try {
const response = await fetch(url, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.text();
} catch (error) {
if (signal.aborted) {
console.warn('Report cancelled:', signal.reason);
}
throw error;
}
}
const controller = new AbortController();
loadReport('/reports/today', controller.signal);
// controller.abort(new Error('View closed')); Frame records across arbitrary response chunks
response.body is a ReadableStream of Uint8Array chunks. A chunk is not guaranteed to match a server write, UTF-8 character, line, or JSON value. TextDecoderStream safely preserves split multibyte sequences, but an incremental parser must also retain an incomplete final record between chunks. Awaiting each reader.read naturally limits demand. If processing stops early, cancel the reader so the underlying fetch can stop, and release an explicitly acquired lock in finally. Check response.body because responses without bodies and some environments can expose null.
async function* readNdjson(response) {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (!response.body) throw new Error('Streaming body unavailable');
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
let completed = false;
try {
while (true) {
const { value = '', done } = await reader.read();
buffer += value;
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) if (line.trim()) yield JSON.parse(line);
if (done) { completed = true; break; }
}
if (buffer.trim()) yield JSON.parse(buffer);
} finally {
if (!completed) await reader.cancel().catch(() => {});
reader.releaseLock();
}
}
const response = await fetch('/events.ndjson');
for await (const event of readNdjson(response)) renderEvent(event); Preserve backpressure and respect one-use body ownership
A Body stream becomes locked when a reader is attached or a pipe owns it, and becomes disturbed once reading or cancellation starts. bodyUsed reports disturbance, not every possible lock. A locked or disturbed body cannot be consumed again or cloned. clone must happen first and tees the stream, but tee backpressure follows the faster consumer while unread bytes for the slower branch can accumulate without a fixed bound. Avoid cloning huge responses for consumers that run at very different speeds. pipeThrough, pipeTo, and awaited reads preserve backpressure; custom producers should enqueue from pull or honor desiredSize instead of eagerly filling memory.
const destination = new WritableStream({
async write(chunk) {
await persistChunk(chunk);
},
close() {
console.log('Response fully persisted');
},
abort(reason) {
console.error('Persistence stopped', reason);
}
}, new ByteLengthQueuingStrategy({ highWaterMark: 64 * 1024 }));
const response = await fetch('/exports/archive.bin', { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (!response.body) throw new Error('Streaming body unavailable');
await response.body.pipeTo(destination, { signal }); Deploy request streaming only with a tested fallback
A ReadableStream request body requires a non-GET/HEAD method and duplex: half; half means the browser completes the upload before exposing the response, not full duplex. Text must be encoded to bytes. Request streaming is limited to same-origin or cors, forces CORS preflight, cannot combine with keepalive, and a one-shot stream cannot generally be replayed across redirects or authentication challenges. The Fetch Standard also restricts streaming requests over HTTP/1.x. Browser, proxy, and origin support remains less uniform than streamed responses, so feature-test the complete deployment path and retain a Blob, FormData, or fixed-byte fallback.
async function uploadLines(lines) {
const supportsRequestStreams = (() => {
try {
let accessed = false;
new Request(location.href, { method: 'POST', body: new ReadableStream(), get duplex() { accessed = true; return 'half'; } });
return accessed;
} catch {
return false;
}
})();
let index = 0;
const stream = new ReadableStream({
pull(controller) {
if (index === lines.length) return controller.close();
controller.enqueue(new TextEncoder().encode(`${lines[index++]}\n`));
}
});
const fallback = new Blob([lines.join('\n'), '\n'], { type: 'text/plain;charset=UTF-8' });
return fetch('/imports/lines', {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
body: supportsRequestStreams ? stream : fallback,
...(supportsRequestStreams ? { duplex: 'half' } : {})
});
} Local code tester
Parse fragmented NDJSON from a local stream
A self-contained Response deliberately splits JSON records across chunks to demonstrate safe incremental UTF-8 decoding without network access.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



