Fetch API Complete Reference: All Options, Request, and Response Objects
Introduction
Throughout the previous articles in this series, you have used fetch() to make GET requests, send JSON data, track download progress, abort requests, and navigate CORS. Each article focused on specific aspects. This article brings everything together into a complete reference for the Fetch API.
The fetch() function accepts an extensive options object with properties that control every aspect of the HTTP request: the method, headers, body, caching behavior, redirect handling, referrer policy, credentials, cancellation, and more. Beyond the options, the API provides two full-featured objects, Request and Response, that encapsulate all the details of the HTTP exchange and can be created, cloned, and passed around independently.
This guide serves as your go-to reference. Each option is explained with its possible values, default behavior, and practical examples. The Request and Response objects are documented with all their properties and methods. Bookmark this page and come back whenever you need to look up a specific fetch() capability.
The fetch() Function Signature
const response = await fetch(resource);
const response = await fetch(resource, options);
Parameters:
resource: A string URL, aURLobject, or aRequestobjectoptions: An optional object containing request settings
Returns: A Promise<Response> that resolves when the server responds with headers (the body may still be downloading).
// String URL
await fetch('https://api.example.com/data');
// URL object
const url = new URL('/data', 'https://api.example.com');
await fetch(url);
// Request object
const request = new Request('https://api.example.com/data', {
method: 'POST',
body: JSON.stringify({ key: 'value' })
});
await fetch(request);
All fetch() Options
The options object can include any of the following properties. Every property is optional. When omitted, each uses its default value.
fetch(url, {
method: 'GET',
headers: {},
body: undefined,
mode: 'cors',
credentials: 'same-origin',
cache: 'default',
redirect: 'follow',
referrer: 'about:client',
referrerPolicy: '',
integrity: '',
signal: undefined,
keepalive: false,
priority: 'auto',
});
method
The HTTP method for the request.
Type: string
Default: 'GET'
fetch(url, { method: 'GET' }); // Retrieve data
fetch(url, { method: 'POST' }); // Create a resource
fetch(url, { method: 'PUT' }); // Replace a resource entirely
fetch(url, { method: 'PATCH' }); // Partially update a resource
fetch(url, { method: 'DELETE' }); // Remove a resource
fetch(url, { method: 'HEAD' }); // Same as GET but no response body
fetch(url, { method: 'OPTIONS' }); // Query server capabilities
The method string is case-insensitive for standard methods. The browser normalizes 'get', 'Get', and 'GET' to 'GET'. However, non-standard methods retain their original casing.
// These are equivalent
fetch(url, { method: 'post' });
fetch(url, { method: 'POST' });
// GET and HEAD requests cannot have a body
fetch(url, { method: 'GET', body: 'data' });
// TypeError: Request with GET/HEAD method cannot have body
headers
HTTP headers to send with the request.
Type: Headers object, plain object, or array of [name, value] pairs
Default: {}
// Plain object (most common)
fetch(url, {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
'Accept': 'application/json',
'X-Custom-Header': 'custom-value'
}
});
// Headers object
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Authorization', 'Bearer token123');
fetch(url, { headers });
// Array of pairs
fetch(url, {
headers: [
['Content-Type', 'application/json'],
['Authorization', 'Bearer token123']
]
});
Certain headers are forbidden and cannot be set by JavaScript. The browser manages them automatically for security:
// These are silently ignored if you try to set them:
// Host, Origin, Referer (partially), Cookie,
// Connection, Content-Length, Transfer-Encoding,
// and several others prefixed with Sec- or Proxy-
When sending FormData as the body, do not set the Content-Type header. The browser must set it automatically to include the multipart boundary string:
const formData = new FormData(formElement);
// WRONG: breaks the request
fetch(url, {
headers: { 'Content-Type': 'multipart/form-data' },
body: formData
});
// CORRECT: browser sets Content-Type with boundary
fetch(url, { body: formData });
body
The request body. Only allowed for methods that support a body (POST, PUT, PATCH, etc.).
Type: string, FormData, Blob, ArrayBuffer, TypedArray, DataView, URLSearchParams, ReadableStream, or null
Default: undefined
// JSON string
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice', age: 30 })
});
// FormData (for forms and file uploads)
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('description', 'My photo');
fetch(url, { method: 'POST', body: formData });
// URLSearchParams (application/x-www-form-urlencoded)
const params = new URLSearchParams();
params.set('username', 'alice');
params.set('password', 'secret');
fetch(url, { method: 'POST', body: params });
// Content-Type is set automatically to application/x-www-form-urlencoded
// Blob (binary data)
const blob = new Blob(['file content'], { type: 'text/plain' });
fetch(url, { method: 'POST', body: blob });
// ArrayBuffer or TypedArray
const buffer = new Uint8Array([72, 101, 108, 108, 111]);
fetch(url, { method: 'POST', body: buffer });
// ReadableStream (streaming upload)
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk 1'));
controller.enqueue(new TextEncoder().encode('chunk 2'));
controller.close();
}
});
fetch(url, { method: 'POST', body: stream, duplex: 'half' });
Some body types automatically set the Content-Type header if you do not provide one:
| Body Type | Auto Content-Type |
|---|---|
string | text/plain;charset=UTF-8 |
URLSearchParams | application/x-www-form-urlencoded;charset=UTF-8 |
FormData | multipart/form-data; boundary=... |
Blob | Uses the Blob's type property |
| Others | No automatic Content-Type |
mode
Controls the CORS behavior of the request.
Type: string
Default: 'cors'
// "cors": Default. Cross-origin requests follow CORS protocol.
// Server must include Access-Control-Allow-Origin in response.
fetch('https://other-domain.com/api', { mode: 'cors' });
// "same-origin": Only same-origin requests are allowed.
// Cross-origin requests throw a TypeError immediately.
fetch('https://other-domain.com/api', { mode: 'same-origin' });
// TypeError: Failed to fetch (if cross-origin)
// "no-cors": Allows cross-origin requests but returns an "opaque" response.
// The response body is empty, status is 0, headers are inaccessible.
// Only useful for Service Workers caching cross-origin assets.
fetch('https://other-domain.com/image.png', { mode: 'no-cors' });
// response.type === "opaque", response.status === 0
// "navigate": Used by the browser for navigation. Not for fetch() in your code.
| Mode | Cross-Origin? | Response Readable? | Use Case |
|---|---|---|---|
cors | Yes (with server permission) | Yes | API calls (default) |
same-origin | No | Yes (same-origin only) | Strict same-origin enforcement |
no-cors | Yes (limited) | No (opaque) | Service Worker caching |
credentials
Controls whether cookies, authorization headers, and TLS client certificates are sent with the request and whether Set-Cookie headers in the response are honored.
Type: string
Default: 'same-origin'
// "omit": Never send or receive credentials.
fetch(url, { credentials: 'omit' });
// "same-origin": Send credentials only for same-origin requests.
// Default behavior.
fetch(url, { credentials: 'same-origin' });
// "include": Always send credentials, even for cross-origin requests.
// Server must respond with Access-Control-Allow-Credentials: true
// and a specific origin in Access-Control-Allow-Origin (not *).
fetch('https://api.example.com/data', { credentials: 'include' });
Practical example showing when each value matters:
// Page is served from https://mysite.com
// Same-origin request to https://mysite.com/api/data
fetch('/api/data');
// credentials: 'same-origin' (default) → cookies ARE sent
// Cross-origin request to https://api.mysite.com/data
fetch('https://api.mysite.com/data');
// credentials: 'same-origin' (default) → cookies NOT sent (different origin)
// Cross-origin request WITH cookies
fetch('https://api.mysite.com/data', { credentials: 'include' });
// cookies ARE sent (requires server CORS headers)
cache
Controls how the request interacts with the browser's HTTP cache.
Type: string
Default: 'default'
// "default": Follow standard HTTP caching rules.
// Browser checks cache, uses it if fresh, validates with server if stale.
fetch(url, { cache: 'default' });
// "no-store": Bypass cache completely. Don't read from cache, don't write to cache.
// Fresh data every time, no caching at all.
fetch(url, { cache: 'no-store' });
// "no-cache": Always validate with the server before using cached response.
// Sends conditional request (If-None-Match / If-Modified-Since).
// Server responds 304 Not Modified if unchanged, 200 with new data if changed.
fetch(url, { cache: 'no-cache' });
// "force-cache": Use cached response if available, even if stale.
// Only makes a network request if nothing is in the cache.
fetch(url, { cache: 'force-cache' });
// "only-if-cached": Only return cached response. Fails if not in cache.
// Can only be used with mode: 'same-origin'.
fetch(url, { cache: 'only-if-cached', mode: 'same-origin' });
// "reload": Ignore cache entirely for reading, but update cache with response.
// Always fetches from network, stores result in cache.
fetch(url, { cache: 'reload' });
When to use each:
// Real-time data that must always be fresh
const stockPrice = await fetch('/api/stock/AAPL', { cache: 'no-store' });
// Data that changes rarely but must be accurate when it does
const userProfile = await fetch('/api/profile', { cache: 'no-cache' });
// Static resources that almost never change
const config = await fetch('/config.json', { cache: 'force-cache' });
// Force refresh after user action
const refreshed = await fetch('/api/notifications', { cache: 'reload' });
| Value | Reads Cache? | Writes Cache? | Network? | Best For |
|---|---|---|---|---|
default | Yes | Yes | If stale | General use |
no-store | No | No | Always | Sensitive/real-time data |
no-cache | Conditional | Yes | Always (validation) | Data freshness with efficiency |
force-cache | Yes (even stale) | Yes | Only if no cache | Offline-tolerant data |
only-if-cached | Yes (only) | No | Never | Offline-only access |
reload | No | Yes | Always | Force fresh fetch |
redirect
Controls how HTTP redirects (301, 302, 303, 307, 308) are handled.
Type: string
Default: 'follow'
// "follow": Automatically follow redirects (up to 20 hops).
// The final response is returned. response.redirected indicates if a redirect occurred.
const response = await fetch(url, { redirect: 'follow' });
console.log(response.redirected); // true if redirected
console.log(response.url); // final URL after redirects
// "error": Treat any redirect as a network error.
// The fetch promise rejects with a TypeError.
try {
await fetch(url, { redirect: 'error' });
} catch (e) {
console.log(e); // TypeError: Failed to fetch (if server redirects)
}
// "manual": Do not follow redirects. Return the redirect response itself.
// The response has type "opaqueredirect": you cannot read the Location header
// or the body. Primarily useful for Service Workers.
const response = await fetch(url, { redirect: 'manual' });
console.log(response.type); // "opaqueredirect"
console.log(response.status); // 0
console.log(response.url); // ""
Practical example for detecting redirects:
const response = await fetch('https://example.com/old-page', {
redirect: 'follow'
});
if (response.redirected) {
console.log(`Redirected to: ${response.url}`);
// You might want to update the URL in the address bar or state
}
referrer
Controls the Referer header (yes, the HTTP header has a historical misspelling) sent with the request.
Type: string
Default: 'about:client' (use the current page's URL)
// Default: send the current page URL as the referrer
fetch(url, { referrer: 'about:client' });
// Sends: Referer: https://mysite.com/current-page
// Custom referrer URL (must be same-origin)
fetch(url, { referrer: 'https://mysite.com/custom-page' });
// Sends: Referer: https://mysite.com/custom-page
// Empty string: send no referrer at all
fetch(url, { referrer: '' });
// No Referer header is sent
referrerPolicy
Controls how much referrer information is included in the Referer header.
Type: string
Default: '' (use the default policy, typically 'strict-origin-when-cross-origin')
// "no-referrer": Never send the Referer header.
fetch(url, { referrerPolicy: 'no-referrer' });
// "no-referrer-when-downgrade": Send full URL, except when going HTTPS → HTTP.
fetch(url, { referrerPolicy: 'no-referrer-when-downgrade' });
// "origin": Send only the origin (protocol + host), not the full path.
// https://mysite.com/page/123 → Referer: https://mysite.com/
fetch(url, { referrerPolicy: 'origin' });
// "origin-when-cross-origin": Full URL for same-origin, origin only for cross-origin.
fetch(url, { referrerPolicy: 'origin-when-cross-origin' });
// "same-origin": Send referrer only for same-origin requests, none for cross-origin.
fetch(url, { referrerPolicy: 'same-origin' });
// "strict-origin": Send origin only (not path), but nothing for HTTPS → HTTP.
fetch(url, { referrerPolicy: 'strict-origin' });
// "strict-origin-when-cross-origin": Full URL for same-origin, origin for cross-origin,
// nothing for HTTPS → HTTP. This is the browser's default policy.
fetch(url, { referrerPolicy: 'strict-origin-when-cross-origin' });
// "unsafe-url": Always send the full URL (including path and query).
// Not recommended: can leak sensitive information.
fetch(url, { referrerPolicy: 'unsafe-url' });
A practical scenario:
// Your page: https://mysite.com/users/42/settings?tab=security
// Different policies send different Referer values:
// "no-referrer" → (none)
// "origin" → https://mysite.com/
// "strict-origin-when-cross-origin"→ https://mysite.com/ (cross-origin)
// → https://mysite.com/users/42/settings?tab=security (same-origin)
// "unsafe-url" → https://mysite.com/users/42/settings?tab=security (always)
integrity
Enables Subresource Integrity (SRI) verification. The browser checks that the downloaded resource matches a known cryptographic hash. If it does not match, the fetch fails.
Type: string (hash algorithm prefix + Base64-encoded hash)
Default: ''
// Verify that the response matches the expected hash
const response = await fetch('https://cdn.example.com/library.js', {
integrity: 'sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE='
});
// If the content doesn't match the hash, fetch rejects with a TypeError
// This protects against CDN compromises or tampering
// Multiple hashes (browser uses the strongest one it supports)
fetch(url, {
integrity: 'sha256-abc123... sha384-def456... sha512-ghi789...'
});
This is primarily used for loading third-party scripts and stylesheets from CDNs, ensuring that a compromised CDN cannot serve malicious code.
signal
An AbortSignal that allows you to cancel the request.
Type: AbortSignal or null
Default: null
// Manual abort with AbortController
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
}
});
controller.abort(); // Cancel the request
// Timeout with AbortSignal.timeout()
fetch(url, { signal: AbortSignal.timeout(5000) });
// Automatically aborts after 5 seconds with TimeoutError
// Combined: manual abort + timeout
const controller = new AbortController();
const signal = AbortSignal.any([
controller.signal,
AbortSignal.timeout(10000)
]);
fetch(url, { signal });
keepalive
Allows the request to outlive the page that initiated it. When keepalive is true, the browser continues the request even if the user navigates away or closes the tab.
Type: boolean
Default: false
// Send analytics data when the user leaves the page
window.addEventListener('beforeunload', () => {
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify({
event: 'page_leave',
duration: getTimeOnPage(),
scrollDepth: getScrollDepth()
}),
headers: { 'Content-Type': 'application/json' },
keepalive: true // Request survives page unload
});
});
Limitations of keepalive:
- The body size is limited (typically 64 KB across all keepalive requests from a page)
- You cannot read the response (the page may already be gone)
- Intended for "fire and forget" requests like analytics and logging
The navigator.sendBeacon() API is a simpler alternative for the same use case. It uses keepalive internally and has a more straightforward interface:
window.addEventListener('beforeunload', () => {
navigator.sendBeacon('/api/analytics', JSON.stringify({
event: 'page_leave'
}));
});
Use sendBeacon() for simple fire-and-forget POSTs. Use fetch() with keepalive: true when you need more control over headers, method, or other options.
priority
Hints to the browser about the relative importance of this request compared to other requests from the same page.
Type: string
Default: 'auto'
// "high": Prioritize this request (e.g., critical API data for rendering)
fetch('/api/critical-data', { priority: 'high' });
// "low": De-prioritize this request (e.g., prefetching, analytics)
fetch('/api/analytics', { priority: 'low' });
// "auto": Let the browser decide based on context (default)
fetch('/api/data', { priority: 'auto' });
Practical example with different priorities:
async function loadPage() {
// Critical data needed for rendering: fetch first
const mainDataPromise = fetch('/api/main-content', { priority: 'high' });
// Important but not blocking
const sidebarPromise = fetch('/api/sidebar', { priority: 'auto' });
// Nice to have, can wait
const recommendationsPromise = fetch('/api/recommendations', { priority: 'low' });
// Analytics: lowest priority
fetch('/api/track-pageview', {
method: 'POST',
priority: 'low',
body: JSON.stringify({ page: location.pathname })
});
const mainData = await mainDataPromise.then(r => r.json());
renderMainContent(mainData);
const sidebar = await sidebarPromise.then(r => r.json());
renderSidebar(sidebar);
const recs = await recommendationsPromise.then(r => r.json());
renderRecommendations(recs);
}
The priority option is a hint, not a command. The browser may not always honor it, and different browsers may interpret priorities differently. It is part of the Fetch Priority API (also known as Priority Hints) and has good support in modern browsers.
duplex
Required when using a ReadableStream as the request body. Controls whether the request and response can be processed simultaneously.
Type: string
Default: Not set (required only for streaming bodies)
// Streaming request body (must set duplex: 'half')
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('streaming data'));
controller.close();
}
});
fetch(url, {
method: 'POST',
body: stream,
duplex: 'half' // Required for ReadableStream bodies
});
'half' means the request body is sent completely before the response begins. Full duplex (simultaneous send and receive) is not yet standardized for fetch.
Complete Options Summary Table
| Option | Type | Default | Purpose |
|---|---|---|---|
method | string | 'GET' | HTTP method |
headers | Headers/object/array | {} | Request headers |
body | string/FormData/Blob/... | undefined | Request body |
mode | string | 'cors' | CORS mode |
credentials | string | 'same-origin' | Cookie/auth handling |
cache | string | 'default' | Cache behavior |
redirect | string | 'follow' | Redirect handling |
referrer | string | 'about:client' | Referrer URL |
referrerPolicy | string | '' | Referrer detail level |
integrity | string | '' | SRI hash |
signal | AbortSignal | null | Cancellation signal |
keepalive | boolean | false | Survive page unload |
priority | string | 'auto' | Request priority hint |
duplex | string | (unset) | Streaming body mode |
The Request Object
The Request object represents an HTTP request. You can create one explicitly and pass it to fetch(), or fetch() creates one internally from the URL and options you provide.
Creating a Request
// Basic request
const request = new Request('https://api.example.com/data');
// Request with options (same options as fetch())
const request = new Request('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' }),
credentials: 'include',
signal: controller.signal
});
// Pass to fetch
const response = await fetch(request);
Creating a Request from Another Request
You can clone and modify an existing request:
const baseRequest = new Request('https://api.example.com/data', {
headers: { 'Authorization': 'Bearer token123' },
credentials: 'include'
});
// Clone with modifications
const modifiedRequest = new Request(baseRequest, {
method: 'POST',
body: JSON.stringify({ name: 'Alice' })
});
// modifiedRequest inherits headers and credentials from baseRequest
// but has a new method and body
Request Properties
All properties are read-only:
const request = new Request('https://api.example.com/data?page=2', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
body: JSON.stringify({ name: 'Alice' }),
mode: 'cors',
credentials: 'include',
cache: 'no-cache',
redirect: 'follow',
referrer: 'about:client',
referrerPolicy: 'strict-origin-when-cross-origin',
integrity: '',
signal: null,
keepalive: false
});
// URL and method
console.log(request.url); // "https://api.example.com/data?page=2"
console.log(request.method); // "POST"
// Headers
console.log(request.headers); // Headers object
console.log(request.headers.get('Content-Type')); // "application/json"
// CORS and credentials
console.log(request.mode); // "cors"
console.log(request.credentials); // "include"
// Caching and redirect
console.log(request.cache); // "no-cache"
console.log(request.redirect); // "follow"
// Referrer
console.log(request.referrer); // "about:client"
console.log(request.referrerPolicy); // "strict-origin-when-cross-origin"
// Security and lifecycle
console.log(request.integrity); // ""
console.log(request.signal); // AbortSignal
console.log(request.keepalive); // false
// Body state
console.log(request.bodyUsed); // false (becomes true after reading)
console.log(request.body); // ReadableStream (the raw body stream)
// Destination (set by browser for navigation, empty for fetch)
console.log(request.destination); // ""
Request Body Methods
The Request object has the same body-reading methods as Response:
const request = new Request(url, {
method: 'POST',
body: JSON.stringify({ name: 'Alice' })
});
// Read the body (can only be done once)
const data = await request.json();
console.log(data); // { name: "Alice" }
console.log(request.bodyUsed); // true
// Other body-reading methods:
// await request.text()
// await request.blob()
// await request.arrayBuffer()
// await request.formData()
Cloning a Request
Since a request's body can only be read once, cloning is essential when you need to read the body or use the request multiple times:
const original = new Request(url, {
method: 'POST',
body: JSON.stringify({ data: 'test' })
});
// Clone before consuming
const clone = original.clone();
// Use the original
await fetch(original);
// Use the clone for retry
await fetch(clone);
Practical Use Case: Request Interceptor
A common pattern is creating a request wrapper that modifies requests before sending them:
async function authenticatedFetch(input, init = {}) {
// Create a Request from whatever was passed
const request = new Request(input, init);
// Create a new request with auth header added
const authenticatedRequest = new Request(request, {
headers: new Headers({
...Object.fromEntries(request.headers),
'Authorization': `Bearer ${getToken()}`
})
});
const response = await fetch(authenticatedRequest);
// Handle token expiration
if (response.status === 401) {
await refreshToken();
// Clone needed because the original body was consumed
const retryRequest = new Request(input, {
...init,
headers: {
...init.headers,
'Authorization': `Bearer ${getToken()}`
}
});
return fetch(retryRequest);
}
return response;
}
// Usage (same interface as fetch)
const response = await authenticatedFetch('/api/data');
const data = await response.json();
The Response Object
The Response object represents the HTTP response returned by fetch(). You can also create Response objects manually (useful in Service Workers and testing).
Response Properties
const response = await fetch('https://api.example.com/data');
// Status information
console.log(response.status); // 200 (HTTP status code)
console.log(response.statusText); // "OK" (HTTP status text)
console.log(response.ok); // true (status is 200-299)
// URL and redirect info
console.log(response.url); // "https://api.example.com/data" (final URL)
console.log(response.redirected); // false (true if a redirect occurred)
// Response type
console.log(response.type);
// "basic": same-origin response
// "cors": cross-origin response with CORS headers
// "opaque": cross-origin response from no-cors request
// "opaqueredirect": redirect response from manual redirect mode
// "error": network error
// Headers
console.log(response.headers); // Headers object
console.log(response.headers.get('Content-Type')); // "application/json"
// Body state
console.log(response.body); // ReadableStream
console.log(response.bodyUsed); // false (becomes true after reading)
| Property | Type | Description |
|---|---|---|
status | number | HTTP status code (0-599) |
statusText | string | HTTP status text |
ok | boolean | true if status is 200-299 |
headers | Headers | Response headers |
url | string | Final response URL |
redirected | boolean | Whether a redirect occurred |
type | string | Response type (basic, cors, opaque, etc.) |
body | ReadableStream | Raw body stream |
bodyUsed | boolean | Whether the body has been consumed |
Response Body Methods
Each method reads the entire body and returns a Promise. The body can only be consumed once:
const response = await fetch(url);
// Parse body as JSON → JavaScript object
const json = await response.json();
// Or read as plain text → string
const text = await response.text();
// Or read as binary blob → Blob object
const blob = await response.blob();
// Or read as raw binary → ArrayBuffer
const buffer = await response.arrayBuffer();
// Or parse as form data → FormData
const formData = await response.formData();
// Body can only be read ONCE
const response = await fetch(url);
const json = await response.json(); // Works
const text = await response.text(); // TypeError: body stream already read
// To read multiple times, clone first
const response = await fetch(url);
const clone = response.clone();
const json = await response.json(); // Read original
const text = await clone.text(); // Read clone
Cloning a Response
const response = await fetch(url);
// Clone creates an independent copy
const clone = response.clone();
// Read both independently
const data1 = await response.json();
const data2 = await clone.json();
// Both contain the same data
You must clone the response before reading the body. Once bodyUsed is true, cloning throws an error:
const response = await fetch(url);
const data = await response.json(); // Body consumed
const clone = response.clone(); // TypeError: body stream is locked
Creating Response Objects Manually
You can construct Response objects directly. This is primarily used in Service Workers for creating custom responses:
// Simple text response
const textResponse = new Response('Hello, World!', {
status: 200,
statusText: 'OK',
headers: { 'Content-Type': 'text/plain' }
});
const text = await textResponse.text();
console.log(text); // "Hello, World!"
// JSON response
const jsonResponse = new Response(
JSON.stringify({ message: 'Success', data: [1, 2, 3] }),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
const data = await jsonResponse.json();
console.log(data.message); // "Success"
// Error response
const errorResponse = new Response(
JSON.stringify({ error: 'Not found' }),
{ status: 404, statusText: 'Not Found' }
);
console.log(errorResponse.ok); // false
console.log(errorResponse.status); // 404
// Response from a Blob
const blob = new Blob(['<h1>Hello</h1>'], { type: 'text/html' });
const htmlResponse = new Response(blob);
Static Response Methods
// Response.error(): Creates a network error response
const errorResponse = Response.error();
console.log(errorResponse.type); // "error"
console.log(errorResponse.status); // 0
// Response.redirect(url, status): Creates a redirect response
const redirect = Response.redirect('https://example.com', 301);
console.log(redirect.status); // 301
console.log(redirect.headers.get('Location')); // "https://example.com"
// Response.json(data, init): Creates a JSON response (modern convenience)
const jsonResponse = Response.json(
{ name: 'Alice', age: 30 },
{ status: 200, headers: { 'X-Custom': 'value' } }
);
const result = await jsonResponse.json();
console.log(result.name); // "Alice"
Response.json() is a modern convenience method that handles JSON.stringify() and sets the Content-Type: application/json header automatically.
The Headers Object
Both Request and Response use the Headers object for managing HTTP headers.
Creating Headers
// From a plain object
const headers = new Headers({
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
});
// Empty, then populated
const headers = new Headers();
headers.set('Content-Type', 'application/json');
// From another Headers object
const copy = new Headers(existingHeaders);
// From array of pairs
const headers = new Headers([
['Content-Type', 'application/json'],
['Accept', 'application/json']
]);
Headers Methods
const headers = new Headers();
// set(name, value): Set a header (replaces existing)
headers.set('Content-Type', 'application/json');
headers.set('Content-Type', 'text/plain'); // Replaces previous value
// append(name, value): Add a value (allows multiple values per header)
headers.append('Accept', 'application/json');
headers.append('Accept', 'text/html');
// Accept is now "application/json, text/html"
// get(name): Get header value (combined if multiple)
console.log(headers.get('Accept')); // "application/json, text/html"
console.log(headers.get('Missing')); // null
// has(name): Check if header exists
console.log(headers.has('Content-Type')); // true
console.log(headers.has('X-Custom')); // false
// delete(name): Remove a header
headers.delete('Accept');
console.log(headers.has('Accept')); // false
// getSetCookie(): Get all Set-Cookie headers as an array
// (unique because Set-Cookie headers shouldn't be combined)
const cookies = headers.getSetCookie();
// Iteration
for (const [name, value] of headers) {
console.log(`${name}: ${value}`);
}
for (const name of headers.keys()) {
console.log(name);
}
for (const value of headers.values()) {
console.log(value);
}
headers.forEach((value, name) => {
console.log(`${name}: ${value}`);
});
Header names are case-insensitive. headers.get('content-type') and headers.get('Content-Type') return the same value. When iterating, names are returned in lowercase.
Putting It All Together: A Production-Ready Fetch Wrapper
Here is a comprehensive fetch wrapper that uses most of the options covered in this reference:
class HttpClient {
constructor(baseURL, defaultOptions = {}) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Accept': 'application/json',
...defaultOptions.headers
};
this.defaultOptions = {
credentials: defaultOptions.credentials || 'same-origin',
mode: defaultOptions.mode || 'cors',
cache: defaultOptions.cache || 'default',
redirect: defaultOptions.redirect || 'follow',
};
this.interceptors = [];
}
addInterceptor(fn) {
this.interceptors.push(fn);
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
// Build headers, merging defaults with request-specific
const headers = new Headers(this.defaultHeaders);
const requestHeaders = new Headers(options.headers || {});
for (const [key, value] of requestHeaders) {
headers.set(key, value);
}
// Auto-stringify body and set Content-Type for objects
let body = options.body;
if (body && typeof body === 'object' && !(body instanceof FormData)
&& !(body instanceof Blob) && !(body instanceof ArrayBuffer)
&& !(body instanceof URLSearchParams) && !(body instanceof ReadableStream)) {
body = JSON.stringify(body);
if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
}
// Build final config
const config = {
...this.defaultOptions,
...options,
headers,
body
};
// Apply interceptors
let request = new Request(url, config);
for (const interceptor of this.interceptors) {
request = await interceptor(request) || request;
}
// Execute fetch
const response = await fetch(request);
// Handle errors
if (!response.ok) {
const errorBody = await response.text().catch(() => '');
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.statusText = response.statusText;
error.body = errorBody;
error.response = response;
throw error;
}
// Handle empty responses
if (response.status === 204 || response.headers.get('Content-Length') === '0') {
return null;
}
// Parse based on Content-Type
const contentType = response.headers.get('Content-Type') || '';
if (contentType.includes('application/json')) {
return response.json();
}
return response.text();
}
get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, body, options = {}) {
return this.request(endpoint, { ...options, method: 'POST', body });
}
put(endpoint, body, options = {}) {
return this.request(endpoint, { ...options, method: 'PUT', body });
}
patch(endpoint, body, options = {}) {
return this.request(endpoint, { ...options, method: 'PATCH', body });
}
delete(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
}
// Usage
const api = new HttpClient('https://api.example.com', {
credentials: 'include',
headers: { 'X-API-Version': '2' }
});
// Add auth interceptor
api.addInterceptor(async (request) => {
const token = await getAuthToken();
const headers = new Headers(request.headers);
headers.set('Authorization', `Bearer ${token}`);
return new Request(request, { headers });
});
// Simple API calls
const users = await api.get('/users');
const newUser = await api.post('/users', {
name: 'Alice',
email: 'alice@example.com'
});
await api.patch('/users/1', { name: 'Updated Alice' });
await api.delete('/users/1');
Summary
The Fetch API provides a comprehensive, Promise-based interface for HTTP requests. The fetch() function accepts an options object with properties for every aspect of the request: method sets the HTTP verb, headers adds custom headers, body carries request data, mode controls CORS behavior, credentials manages cookies and authentication, cache directs caching strategy, redirect handles redirections, referrer and referrerPolicy control the Referer header, integrity enables subresource integrity checks, signal enables cancellation, keepalive allows requests to survive page unloads, and priority hints at request importance.
The Request object encapsulates all request details and can be created, cloned, and passed around. It uses the same options as fetch() and shares the body-reading interface with Response. The Response object provides status information (status, ok, statusText), metadata (url, redirected, type, headers), and body-reading methods (json(), text(), blob(), arrayBuffer(), formData()). The Headers object manages HTTP headers with get, set, append, delete, has, and full iteration support.
Key rules to remember: the body can only be read once (clone first if needed), FormData bodies must not have a manual Content-Type header, GET/HEAD cannot have a body, and fetch() only rejects on network failures (always check response.ok for HTTP errors).