The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Fetch JSONconst response = await fetch('/api/items', { headers: { Accept: 'application/json' } })View examples
Reject an HTTP errorif (!response.ok) throw new Error(`HTTP ${response.status}`)View examples
Consume JSON onceconst data = await response.json()View examples
Construct a Requestconst request = new Request('/api/items', { method: 'GET', cache: 'no-cache' })View examples
Require a CORS responseawait fetch('https://api.example/data', { mode: 'cors' })View examples
Include cross-origin credentialsawait fetch(url, { mode: 'cors', credentials: 'include' })View examples
Enforce same-origin accessawait fetch(url, { mode: 'same-origin' })View examples
Cancel with a controllerconst controller = new AbortController(); fetch(url, { signal: controller.signal })View examples
Apply a deadlineawait fetch(url, { signal: AbortSignal.timeout(5000) })View examples
Combine cancellation sourcesconst signal = AbortSignal.any([controller.signal, AbortSignal.timeout(5000)])View examples
Acquire a byte readerconst reader = response.body.getReader()View examples
Decode streamed textconst textStream = response.body.pipeThrough(new TextDecoderStream())View examples
Pipe with cancellationawait response.body.pipeTo(destination, { signal })View examples
Check body disturbanceif (response.bodyUsed) throw new Error('Body already consumed')View examples
Clone before readingconst auditCopy = response.clone()View examples
Upload a streamawait 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

01

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.

Fetch and validate a JSON collection
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;
}
Back to quick reference ↑
02

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.

Send an explicitly credentialed CORS request
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
Back to quick reference ↑
03

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.

Combine user cancellation with a response deadline
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'));
Back to quick reference ↑
04

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.

Parse newline-delimited JSON without assuming chunk boundaries
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);
Back to quick reference ↑
05

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.

Pipe bytes to a bounded processing sink
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 });
Back to quick reference ↑
06

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.

Stream generated text with a fixed-body 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' } : {})
  });
}
Back to quick reference ↑

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.

Runs in your browser
Preview

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. WHATWGFetch Standardfetch.spec.whatwg.org
  2. WHATWGStreams Standardstreams.spec.whatwg.org
  3. WHATWGDOM Standard: Aborting ongoing activitiesdom.spec.whatwg.org
  4. WHATWGEncoding Standard: TextDecoderStreamencoding.spec.whatwg.org
  5. Internet Engineering Task ForceHTTP Semanticsrfc-editor.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback