◆JavaScript◆Networking◆Advanced◆Intermediate to Advanced
Introduction
The Fetch API provides a modern, promise-based interface for making HTTP requests in the browser. It replaces XMLHttpRequest (XHR) with a cleaner, more flexible API that integrates naturally with service workers, streaming, and other modern web standards. The global fetch() method is the entry point — a simple function that takes a URL and returns a Promise resolving to a Response object.
Unlike XHR, fetch uses promises for asynchronous flow control, making it compatible with async/await syntax and promise chains. It also supports request and response streaming, enabling efficient handling of large payloads. The Fetch API is part of the WHATWG Fetch Standard and is implemented in all modern browsers and Node.js (since v18).
One important distinction: fetch only rejects on network errors (DNS failure, connection refused, etc.). HTTP error statuses like 404 or 500 are treated as successful responses — you must check response.ok or response.status to detect them.
if (!response.ok) throw new Error(`HTTP ${response.status}`);
17
return await response.json();
18
} catch(error) {
19
console.error('Fetch failed:', error);
20
throw error;
21
}
22
}
Basic GET Requests
A GET request retrieves data from a specified resource. It is the default method when no method option is provided. The URL can include query parameters for filtering, sorting, or pagination. Always handle the response correctly by checking the status before processing the body.
basic-get.js
JavaScript
1
// Basic GET with query parameters
2
asyncfunction getUsers(page = 1, limit = 10) {
3
const url = new URL('https://api.example.com/users');
4
url.searchParams.set('page', page.toString());
5
url.searchParams.set('limit', limit.toString());
6
url.searchParams.set('sort', '-createdAt');
7
8
const response = awaitfetch(url.toString());
9
if (!response.ok) {
10
throw new Error(`Failed to fetch users: ${response.status}`);
// cache: 'force-cache', // use cached when available
31
// cache: 'no-store', // never cache
32
});
Request Methods
The Fetch API supports all standard HTTP methods. The method option controls which HTTP verb is used. Each method has semantic meaning — GET retrieves, POST creates, PUT replaces, PATCH updates partially, and DELETE removes. The server may enforce different behavior, validation, and status codes for each method.
if (!response.ok) throw new Error(`Delete failed: ${response.status}`);
40
}
Headers & Body
The Headers interface allows you to set, get, and delete HTTP headers for both requests and responses. Headers are case-insensitive and immutable in responses (unless you clone them). The request body can be a string, FormData, Blob, ArrayBuffer, TypedArray, DataView, URLSearchParams, or ReadableStream.
The Response object provides several methods to consume the body in different formats. Each method returns a promise and can only be called once — the response body is a ReadableStream that can be consumed a single time. Clone the response with response.clone() if you need to read it multiple times.
Method
Returns
Use Case
response.json()
Promise<any>
Parse JSON API responses
response.text()
Promise<string>
Read as plain text (HTML, CSV, XML)
response.blob()
Promise<Blob>
Binary data (images, PDFs, files)
response.formData()
Promise<FormData>
Parse multipart/form-data responses
response.arrayBuffer()
Promise<ArrayBuffer>
Raw binary buffer (audio, WebAssembly)
response-handling.js
JavaScript
1
// JSON response (most common for APIs)
2
asyncfunction fetchJSON(url) {
3
const res = await fetch(url);
4
if (!res.ok) throw new Error(`HTTP ${res.status}`);
5
return res.json();// automatically parses JSON
6
}
7
8
// Text response (HTML, CSV, plain text)
9
asyncfunction fetchText(url) {
10
const res = await fetch(url);
11
return res.text();
12
}
13
14
// Blob response (binary files)
15
asyncfunction fetchImage(url) {
16
const res = await fetch(url);
17
const blob = await res.blob();
18
const imgUrl = URL.createObjectURL(blob);
19
document.querySelector('img').src = imgUrl;
20
// Clean up: URL.revokeObjectURL(imgUrl) when done
21
}
22
23
// FormData response
24
asyncfunction fetchFormData(url) {
25
const res = await fetch(url);
26
const formData = await res.formData();
27
for (const [key, value] of formData) {
28
console.log(key, value);
29
}
30
}
31
32
// ArrayBuffer (low-level binary)
33
asyncfunction fetchBuffer(url) {
34
const res = await fetch(url);
35
return res.arrayBuffer(); // useful for WebAssembly, audio processing
console.log('Type:', res.type); // 'basic', 'cors', 'opaque', etc.
44
console.log('Redirected:', res.redirected);
45
console.log('Headers:', [...res.headers]);
46
return res.json();
47
}
Error Handling
One of the most common mistakes with fetch is forgetting that HTTP error responses (4xx, 5xx) do NOT cause the promise to reject. The promise only rejects on network-level failures. You must explicitly check response.ok (true for status 200-299) or inspect response.status directly.
Error Type
Fetch Behavior
Detection
HTTP Error
Resolves normally (not a rejection)
Check response.ok or response.status
Network Error
Promise rejects with TypeError
catch() or try/catch
CORS Error
Network error (opaque response)
type === 'opaque'
Timeout
No built-in — use AbortController
AbortError from signal
JSON Parse Error
Rejects when calling response.json()
catch() on json() call
error-handling.js
JavaScript
1
// Comprehensive error handling
2
asyncfunction robustFetch(url, options = {}) {
3
try {
4
const response = await fetch(url, options);
5
6
// HTTP errors are NOT network errors — check explicitly
Remember: fetch only rejects on network errors, NOT on HTTP errors like 404 or 500. Always check response.ok or response.status. This is a common source of bugs — your .catch() handler will not fire for 404 responses.
AbortController
The AbortController interface allows you to cancel in-flight fetch requests. This is essential for race condition prevention in SPAs, search-as-you-type inputs, page navigation cleanup, and setting request timeouts. When a request is aborted, the fetch promise rejects with an AbortError.
if (error.name === 'AbortError') return; // silently ignore
34
throw error;
35
}
36
}
37
38
// Abort signal for multiple requests
39
const controller = new AbortController();
40
const { signal } = controller;
41
42
// Pass the same signal to multiple requests
43
Promise.all([
44
fetch('/api/users', { signal }),
45
fetch('/api/posts', { signal }),
46
fetch('/api/comments', { signal }),
47
])
48
// Abort all if page navigates away
49
controller.abort();
50
51
// Using AbortController for cleanup in React
52
useEffect(() => {
53
const controller = new AbortController();
54
fetch('/api/data', { signal: controller.signal })
55
.then(res => res.json())
56
.then(setData)
57
.catch(err => {
58
if (err.name !== 'AbortError') setError(err);
59
});
60
return () => controller.abort();
61
}, []);
🔥
pro tip
Use AbortController for more than just timeouts. It is the standard way to cancel fetches when: the user navigates away, a component unmounts, a new search replaces the previous one, or a modal closes. The same signal can be shared across multiple requests to abort them all at once.
Request & Response Objects
The Request and Response objects provide structured interfaces for HTTP communication. Creating a Request object explicitly is useful when you need to reuse the same configuration, modify requests before sending, or pass them to service workers. Both objects implement the Body mixin, providing the same body-reading methods.
request-response.js
JavaScript
1
// Creating and reusing a Request object
2
const request = new Request('https://api.example.com/users', {
3
method: 'POST',
4
headers: {
5
'Content-Type': 'application/json',
6
'Authorization': `Bearer ${token}`,
7
},
8
body: JSON.stringify({ name: 'John' }),
9
cache: 'no-cache',
10
credentials: 'include',
11
});
12
13
// Request objects are immutable — clone to modify
console.log(redirectRes.redirected); // true if followed a redirect
50
console.log(redirectRes.url); // final URL after redirects
CORS & Credentials
Cross-Origin Resource Sharing (CORS) is a security mechanism that controls how resources from one origin can be requested by a different origin. The browser enforces CORS policies based on response headers from the server. The credentials option controls whether cookies, TLS client certificates, and authorization headers are sent with cross-origin requests.
credentials Mode
Cookies Sent
Use Case
same-origin
✓ (same origin only)
Default — safe for APIs on same domain
include
✓ (all origins)
Cross-origin with cookies (requires ACAO header)
omit
✗
Never send credentials
mode Option
Description
cors
Cross-origin allowed (requires server CORS headers)
same-origin
Rejects cross-origin requests entirely
no-cors
Limited cross-origin (opaque response, no body reading from JS)
navigate
For navigation requests (browser internal)
cors-credentials.js
JavaScript
1
// Credentials modes
2
// Same-origin (default) — cookies only for same origin
CORS errors cannot be handled in JavaScript — the browser blocks the response and the fetch promise rejects with a network error. The only fix is on the server side: ensure it sends correct Access-Control-Allow-Origin headers. For development, use a proxy or disable CORS in the browser (not recommended for production).
Streaming Responses
The Fetch API supports streaming response bodies via the ReadableStream interface. Instead of waiting for the entire response to download, you can process chunks as they arrive. This is ideal for large files, real-time data feeds, and progressive rendering. Access the raw stream through response.body.
Unlike XMLHttpRequest, fetch does not have a built-in upload progress event. However, you can implement upload progress tracking using the ReadableStream API or by using XMLHttpRequest for upload-specific scenarios. For download progress, the streaming approach with response.body works well.
upload-progress.js
JavaScript
1
// Upload progress using ReadableStream (advanced)
2
function uploadWithProgress(url, file, onProgress) {
const total = contentLength ? parseInt(contentLength) : 0;
58
let loaded = 0;
59
60
for await (const chunk of streamBody(res)) {
61
loaded += chunk.length;
62
onProgress({ loaded, total });
63
}
64
}
65
66
asyncfunction* streamBody(response) {
67
const reader = response.body.getReader();
68
while(true) {
69
const { done, value } = await reader.read();
70
if (done) return;
71
yield value;
72
}
73
}
Interceptors Pattern
Unlike Axios, fetch does not have built-in interceptors. However, you can implement the interceptor pattern by wrapping fetch with before-request and after-response hooks. This pattern is useful for adding authentication tokens, logging, error normalization, retry logic, and request/response transformation across all API calls.
interceptors.js
JavaScript
1
// Fetch wrapper with interceptors
2
class HttpClient {
3
constructor(baseURL) {
4
this.baseURL = baseURL;
5
this.requestInterceptors = [];
6
this.responseInterceptors = [];
7
}
8
9
// Add a request interceptor (receives Request, returns modified Request)
10
addRequestInterceptor(fn) {
11
this.requestInterceptors.push(fn);
12
return() => {
13
this.requestInterceptors =
14
this.requestInterceptors.filter(i => i !== fn);
15
};
16
}
17
18
// Add a response interceptor (receives Response, returns modified Response)
19
addResponseInterceptor(fn) {
20
this.responseInterceptors.push(fn);
21
return() => {
22
this.responseInterceptors =
23
this.responseInterceptors.filter(i => i !== fn);
24
};
25
}
26
27
async request(url, options = {}) {
28
let request = new Request(
29
`${this.baseURL}${url}`,
30
options
31
);
32
33
// Apply request interceptors
34
for (const interceptor of this.requestInterceptors) {
35
request = await interceptor(request);
36
}
37
38
let response = await fetch(request);
39
40
// Apply response interceptors
41
for (const interceptor of this.responseInterceptors) {
XMLHttpRequest (XHR) was the original API for making HTTP requests in the browser. While fetch is now the modern standard, XHR still has some unique capabilities. Understanding the differences helps you choose the right tool and maintain legacy code.
// 4. When you need abort without AbortController polyfill
✓
best practice
Use fetch for all new projects. It integrates naturally with service workers, streams, promises, and async/await. Reserve XMLHttpRequest for the specific use cases where it still excels: upload progress tracking and legacy browser support. For upload progress with fetch, consider wrapping XHR in a promise as shown above.
Best Practices
◆Always check response.ok — HTTP errors (404, 500) do not reject the fetch promise
◆Use AbortController for timeouts and cleanup — prevents race conditions and memory leaks
◆Centralize fetch logic in a client wrapper with interceptors for auth, logging, and error handling
◆Use response.clone() if you need to read the body multiple times — bodies are single-consumption streams
◆Set Content-Type headers explicitly — fetch does not set them automatically for JSON bodies
◆Handle JSON parse errors separately — response.json() can fail even when the HTTP status is OK
◆Use credentials: 'include' for cross-origin requests that need cookies (server must allow it)
◆Implement retry logic for transient failures (network blips, 429 rate limits, 503 service unavailable)
◆Prefer async/await over .then() chains for better error handling with try/catch
◆Cache responses when appropriate — use the Cache API or HTTP caching headers rather than reinventing