Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
The reliable way to build a browser Fetch API is to wrap the native fetch() function without hiding its policies. Accept a URL or Request, forward RequestInit options such as signal, credentials and cache, check response.ok yourself, and let the caller choose buffered parsing or streaming. HTTP 4xx and 5xx responses normally fulfill the promise; network failures, unsupported schemes and aborts reject it.
The wrapper below gives callers a consistent result, bounded diagnostics for HTTP errors and selectable JSON, text, blob or stream handling. CORS, cookies, cache behavior and security remain browser/server decisions that a wrapper cannot bypass.
Start with a small, policy-preserving wrapper
Keep the wrapper close to the platform API. Forward a URL or Request and every supported RequestInit field instead of inventing a second request language. The only behavior worth adding centrally is consistent HTTP-status checking and an application error that retains useful, non-sensitive diagnostics.
Recommended Free Tools
export class HttpError extends Error {
constructor(message, details) {
super(message);
this.name = 'HttpError';
Object.assign(this, details);
}
}
export async function request(resource, init = {}) {
let response;
try {
response = await fetch(resource, init);
} catch (error) {
// TypeError commonly represents a network/CORS failure; AbortError means cancellation.
throw error;
}
if (!response.ok) {
let detail = '';
try {
// Bound diagnostics so an error page cannot consume unbounded memory.
detail = (await response.text()).slice(0, 4000);
} catch {
detail = '[response body unavailable]';
}
throw new HttpError(`HTTP ${response.status} ${response.statusText}`, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
detail,
response
});
}
return response;
}
export async function getJson(resource, init = {}) {
const response = await request(resource, {
...init,
headers: { Accept: 'application/json', ...(init.headers || {}) }
});
return response.json();
}
Callers can use await getJson('/api/profile'), or call request() and decide how to consume the body. Returning the original Response preserves status, headers, URL, redirects and the stream.
#1 Best Overall
Why response.ok matters
fetch() fulfills its promise for an HTTP response even when the status is 404, 401, 429 or 500. The ok property is true only for statuses in the 200–299 range. A wrapper that immediately calls response.json() can therefore turn an HTML error page into a confusing parse error. Check status first, then parse according to the endpoint contract.
Accept both URLs and Request objects
A Request carries method, headers, mode, credentials, cache, redirect and signal settings. Accepting it lets higher-level code prepare a request once, clone it for retries when its body is replayable, and pass it through the same error handling.
Choose buffered parsing or a stream
Convenience readers for ordinary responses
response.json(), response.text() and response.blob() read the complete body before resolving. They are the simplest choice for small API responses, configuration documents and images. They also make peak memory roughly proportional to the entire payload, and the caller receives no usable data until the body finishes.
Free tools Windows power users keep installed
One-click scans. No signup required.
const user = await getJson('/api/user');
const response = await request('/reports/latest.txt');
const text = await response.text();
const imageResponse = await request('/images/hero.webp');
const imageBlob = await imageResponse.blob();
const objectUrl = URL.createObjectURL(imageBlob);
try {
document.querySelector('#hero').src = objectUrl;
} finally {
// Revoke it when the image is no longer needed.
setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
}
Incremental processing with ReadableStream
Request and response bodies are streams. Read response.body when downloading a large text file, processing newline-delimited JSON, or displaying progressive output. The following example decodes chunks, retains an incomplete final line and stops on cancellation.
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
export async function readNdjson(resource, { signal, onItem } = {}) {
const response = await request(resource, { signal });
if (!response.body) throw new Error('This response has no readable body');
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let pending = '';
try {
for (;;) {
const { value, done } = await reader.read();
if (done) break;
pending += value;
const lines = pending.split('n');
pending = lines.pop();
for (const line of lines) {
const trimmed = line.trim();
if (trimmed) onItem(JSON.parse(trimmed));
}
}
if (pending.trim()) onItem(JSON.parse(pending));
} finally {
reader.releaseLock();
}
}
A stream is single-use. Once a body has been consumed, another reader cannot read it unless you call response.clone() before consumption; cloning can duplicate buffering, so do it only when both consumers truly need the body.
CORS decides whether browser JavaScript can see the response
Cross-origin requests are governed by the server’s CORS headers and the browser’s request mode. The default mode for a cross-origin fetch is cors. A wrapper cannot add permission that the server did not grant.
| Request case | What the browser does | Developer consequence |
|---|---|---|
| Same-origin | Uses the page’s origin policy and can expose a normal response. | Keep API and frontend on one origin when you control deployment. |
| Cross-origin, simple request | The request may be sent directly; the response is exposed only when Access-Control-Allow-Origin matches. |
Configure the API response, not just frontend JavaScript. |
| Cross-origin, non-simple request | Usually sends an OPTIONS preflight asking permission for the method and headers. |
Allow the requested method and headers and return a successful preflight response. |
mode: 'no-cors' |
Returns an opaque response with status 0, unreadable headers and an unreadable body. | It is rarely useful for application data; it does not bypass CORS. |
Typical preflight triggers
Methods other than the browser’s simple methods, non-safelisted request headers such as a custom authorization header, and non-safelisted content types can trigger preflight. A failed preflight appears to script as a fetch rejection rather than an HTTP response you can inspect. Verify the browser Network panel’s OPTIONS request and the server’s Access-Control-Allow-Methods, Access-Control-Allow-Headers and origin response.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsUse a same-origin server proxy when appropriate
If the third-party server cannot enable CORS, call your own same-origin backend and have it retrieve the resource. Restrict allowed destination hosts, validate user input and set timeouts; otherwise the proxy can become a server-side request forgery risk. This changes your deployment topology, bandwidth and authentication model, but it is the dependable alternative to trying to defeat browser policy.
Rank #3
Credentials, cookies and CSRF
The credentials option controls whether cookies, TLS client certificates and other credentials participate. The browser default is same-origin.
credentials |
Use | Important server requirement |
|---|---|---|
'omit' |
Never send credentials. | Useful for public resources and reducing accidental cookie exposure. |
'same-origin' |
Send credentials only to the page’s origin. | Default behavior. |
'include' |
Opt into credentials on cross-origin requests too. | The response must include a specific Access-Control-Allow-Origin value and Access-Control-Allow-Credentials: true; * cannot be used for the origin. |
Cookies still obey their SameSite, Secure and domain attributes. Treat cross-origin include as a security decision: a state-changing endpoint can become vulnerable to cross-site request forgery unless it uses appropriate CSRF defenses, origin checks and SameSite settings. Do not put secrets in a URL merely to avoid credential configuration.
const response = await request('https://api.example.test/account', {
credentials: 'include',
headers: { Accept: 'application/json' }
});
const account = await response.json();
Cancellation and timeouts
Pass an AbortSignal from the caller. Aborting rejects the fetch with an AbortError; if headers arrived but the body is still being read, a later read can also raise AbortError.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →export async function fetchWithTimeout(resource, init = {}, timeoutMs = 15000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await request(resource, { ...init, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
const controller = new AbortController();
window.addEventListener('beforeunload', () => controller.abort(), { once: true });
try {
const response = await request('/api/search?q=browser', { signal: controller.signal });
render(await response.json());
} catch (error) {
if (error.name === 'AbortError') return;
throw error;
}
Abort requests when a component is disposed, navigation makes the result irrelevant, or a deadline expires. Do not automatically retry every abort: cancellation is often an intentional user action.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Make cache policy explicit
RequestInit.cache controls how fetch interacts with the browser HTTP cache. It is a policy choice, not a guarantee that data is fresh or stale.
| Mode | Practical use | Trade-off |
|---|---|---|
default |
Normal browser cache behavior. | Good general default; freshness follows HTTP cache headers. |
no-store |
Do not use or update the HTTP cache. | Freshness at the cost of bandwidth and latency. |
reload |
Fetch from the network and update cache. | Useful for an explicit refresh. |
no-cache |
Revalidate a cached response before using it. | Can save bytes while checking freshness. |
force-cache |
Prefer a cached response, even if stale under normal rules. | Low latency but potentially old data. |
only-if-cached |
Use cache only when an eligible entry exists. | Restricted to same-origin requests and can fail when no entry is available. |
const response = await request('/api/catalog', {
cache: 'no-cache',
headers: { Accept: 'application/json' }
});
A service worker can add application-level caching, offline fallbacks and stale-while-revalidate behavior. Define invalidation and freshness rules explicitly; a service worker should not silently make data appear current.
Error handling, retries and diagnostics
Separate transport failures from HTTP failures
- Rejected promise: network failure, unsupported URL scheme, blocked request or abort. There is no usable HTTP status.
- Fulfilled response with
ok === false: the server answered with 4xx or 5xx. Preserve status and a bounded body for diagnostics. - Parsing error: the status may be successful, but the body is not valid JSON or the content type is not what the endpoint promised.
Log request IDs, status and timing where available, but avoid logging authorization headers, cookies or full bodies that may contain personal data.
Retry only safe, replayable operations
Retries can help transient network failures and selected 408, 429 or 5xx responses, but they multiply load and can duplicate side effects. Retry idempotent GET requests with bounded exponential backoff and honor server rate-limit guidance. For writes, use an idempotency key supported by the API rather than blindly replaying a request body.
Best Value
Common symptoms and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
404 does not enter catch |
HTTP errors fulfill the promise. | Check response.ok or response.status before parsing. |
| “Failed to fetch” with no status | Network failure, DNS/TLS issue, blocked CORS or unsupported scheme. | Inspect the Network and Console panels, verify HTTPS and server CORS headers, and test the URL independently. |
| Preflight returns 401/403 | The server requires authentication on OPTIONS or does not allow the requested method/headers. |
Handle preflight without session-dependent challenges and return the required CORS headers. |
| Response is opaque, status 0 | mode: 'no-cors' was used. |
Remove it and configure CORS, or use a same-origin backend proxy. |
| Cookies are missing | Cross-origin credentials default to same-origin, or cookie SameSite rules prevent sending. |
Use credentials: 'include' only when needed and configure cookie and CORS policies together. |
| JSON parsing fails on a 200 | The server returned HTML, an empty body or malformed JSON. | Inspect Content-Type, read a bounded text() diagnostic and fix the endpoint contract. |
| Large download freezes the tab | json() or text() buffers the entire body. |
Consume response.body incrementally and abort when the result is no longer needed. |
| Repeated calls return old data | HTTP cache or service-worker cache policy. | Choose no-cache, reload or no-store deliberately and verify cache headers. |
Performance and deployment checklist
- Keep API and frontend on the same origin when practical; otherwise document allowed origins and preflight behavior.
- Request only the representation you need with
Accept; avoid downloading a full document when an endpoint can return a compact projection. - Use streaming for large or progressive responses, but keep buffered readers for small payloads where simplicity wins.
- Set a deadline with
AbortControllerand cancel obsolete requests during navigation or component teardown. - Measure time to headers separately from time to complete body; streaming can improve first usable data without reducing total bytes.
- Make cache policy visible in the wrapper’s options so callers can choose freshness versus latency.
- Test success, 404, 429, 500, malformed JSON, CORS preflight, missing credentials, abort during headers and abort during body reading.
- Use HTTPS in production. Browsers may block mixed-content requests from an HTTPS page.
Or skip the browser setup
If your goal is a clean image or PDF of a web page rather than application data, ScreenshotNeo provides a website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits cost nothing, and response headers identify the page verdict and billing result.
One GET request is enough. See the full parameter reference in the ScreenshotNeo documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo HTTP ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
ScreenshotNeo also offers an MCP server for Claude, Cursor and other MCP clients, with take_screenshot, get_page_info and capture_pdf tools. It supports full-page and element captures, device presets, retina scale, dark mode, PDF controls, custom CSS and JavaScript, click and wait actions, request blocking, headers, cookies, user agents, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage data and an OpenAPI specification. Existing parameter names used by other screenshot APIs also work.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account to get started.
Frequently Asked Questions
Can I call fetch from a Web Worker?
Yes. Fetch is available in Window and Worker contexts, so the same wrapper can run in a dedicated or service worker as long as the worker’s origin and lifetime are handled correctly.
Why can a response body be read only once?
Bodies are streams. After a reader or convenience method consumes the stream, it is locked or disturbed; clone the response before reading when two independent consumers are genuinely required.
Does fetch provide upload-progress events?
The Fetch API does not expose the same upload-progress event model as XMLHttpRequest. For progress-sensitive uploads, evaluate the browser capabilities and protocol you control rather than assuming a wrapper can report byte progress.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

