Fetch Basics
fetch(url) returns a Promise that resolves to a Response object. The response body must be explicitly parsed β the most common method is .json():
// Basic GET with .then() chain
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json()) // parse JSON body
.then(data => console.log(data))
.catch(error => console.error('Network error:', error));
// The same with async/await (cleaner)
async function getPost(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await response.json();
console.log(post);
return post;
}
getPost(1);
{ userId: 1, id: 1, title: "sunt aut facere repellat...", body: "quia et suscipit..." }
The Response Object
The Response object has important properties and multiple body-reading methods:
async function inspectResponse() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
// Status info
console.log(response.status); // 200, 404, 500, etc.
console.log(response.statusText); // "OK", "Not Found", etc.
console.log(response.ok); // true for 200β299
console.log(response.url); // final URL (after redirects)
console.log(response.redirected); // true if URL changed
// Headers
console.log(response.headers.get('Content-Type'));
// "application/json; charset=utf-8"
// Body parsing methods (each returns a Promise, use only ONE per response)
const json = await response.json(); // parses JSON
// const text = await response.text(); // raw string
// const blob = await response.blob(); // binary data (images, files)
// const buf = await response.arrayBuffer(); // binary buffer
// const form = await response.formData(); // multipart form data
}
Once you call response.json(), response.text(), or any body method, the body stream is consumed. Calling a second body method throws an error. If you need to read the body multiple times, clone the response first: const clone = response.clone().
Handling HTTP Errors
Fetch only rejects (goes to catch) on network failures (offline, DNS error). HTTP error codes like 404 or 500 resolve successfully β you must check response.ok:
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
// Must check response.ok for HTTP errors
if (!response.ok) {
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
}
const user = await response.json();
return user;
} catch (error) {
if (error.name === 'TypeError') {
// Network failure (no internet, DNS fail, CORS block)
console.error('Network error β are you offline?');
} else {
// HTTP error or JSON parse error
console.error('Request failed:', error.message);
}
return null;
}
}
// Usage
const user = await fetchUser(42);
if (user) renderUser(user);
POST Request with JSON Body
async function createPost(postData) {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer my-token-here'
},
body: JSON.stringify(postData)
});
if (!response.ok) throw new Error(`Failed: ${response.status}`);
const created = await response.json();
console.log('Created post with ID:', created.id);
return created;
}
createPost({
title: 'Hello Fetch',
body: 'Learning the Fetch API',
userId: 1
});
// PUT, PATCH, DELETE follow the same pattern
async function deletePost(id) {
const response = await fetch(`/api/posts/${id}`, { method: 'DELETE' });
if (response.status !== 204 && !response.ok) {
throw new Error('Delete failed');
}
console.log('Deleted post', id);
}
Created post with ID: 101
Fetch Options Reference
| Option | Type | Description |
|---|---|---|
method | string | "GET" (default), "POST", "PUT", "PATCH", "DELETE", "HEAD" |
headers | object / Headers | Request headers, e.g. Content-Type, Authorization |
body | string / FormData / Blob / URLSearchParams | Request payload; not allowed for GET/HEAD |
mode | string | "cors" (default), "no-cors", "same-origin" |
credentials | string | "same-origin" (default), "include" (send cookies cross-origin), "omit" |
cache | string | "default", "no-store", "reload", "force-cache" |
signal | AbortSignal | For cancellation via AbortController |
Cancellation with AbortController
let currentController = null;
async function search(query) {
// Cancel any in-flight request from previous call
if (currentController) currentController.abort();
currentController = new AbortController();
const { signal } = currentController;
try {
const response = await fetch(
`https://api.example.com/search?q=${encodeURIComponent(query)}`,
{ signal }
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const results = await response.json();
displayResults(results);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled (new search started)');
} else {
console.error('Search failed:', error.message);
}
}
}
// Timeout pattern using AbortController
async function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (e) {
if (e.name === 'AbortError') throw new Error(`Request timed out after ${ms}ms`);
throw e;
}
}
Fetch vs XMLHttpRequest
| Feature | fetch() | XMLHttpRequest |
|---|---|---|
| API style | Promise-based | Callback-based (event listeners) |
| async/await support | Native | Requires wrapping in a Promise |
| Upload progress | Not directly (use XHR or streams) | xhr.upload.onprogress |
| Request cancellation | AbortController | xhr.abort() |
| HTTP error detection | Manual (response.ok) | Manual (status check) |
| Streaming responses | ReadableStream via response.body | Limited |
| Code verbosity | Concise | Verbose |
Cross-Origin Resource Sharing (CORS) is a browser security policy that blocks requests to a different origin (scheme + domain + port) unless the server responds with Access-Control-Allow-Origin headers. Fetch respects CORS automatically β if the server doesn't allow your origin, the browser blocks the response and fetch rejects with a TypeError.
ποΈ Practical Exercise
Build a user profile card loader:
- On page load, fetch all users from
https://jsonplaceholder.typicode.com/users. - Render each user as a card with name, email, and company name.
- Add a "Load Posts" button on each card that fetches
/posts?userId={id}and shows the post titles in a dropdown list. - Handle errors: show a friendly message if the fetch fails.
π₯ Challenge Exercise
Build a debounced live search that queries https://jsonplaceholder.typicode.com/posts?title_like={query} as the user types. Cancel in-flight requests using AbortController. Show a loading spinner during the request, display results as a list, and show "No results found" when the array is empty. Debounce input events by 300 ms.
Summary
π Summary
fetch(url)returns a Promise resolving to aResponseobject.- Always check
response.okβ Fetch only rejects on network failures, not HTTP errors. - Parse the body with
.json(),.text(), or.blob()β each consumes the body stream. - Set
method,headers, andbodyin the options object for POST/PUT/PATCH. - Use
AbortControllerto cancel in-flight requests or implement timeouts. - CORS is enforced by the browser β the server must send appropriate headers for cross-origin requests.
Interview Questions
- Why doesn't fetch reject on a 404 response? How do you handle it?
- How would you cancel a fetch request initiated 500 ms ago?
- What is the difference between
credentials: 'include'and the default? - How do you implement a request timeout with fetch?
- When would you still prefer XMLHttpRequest over fetch?
Frequently Asked Questions
Pass credentials: 'include' in the options object. The server must also respond with Access-Control-Allow-Credentials: true and a specific (non-wildcard) Access-Control-Allow-Origin header. Without both, the browser will block the response.
Use a FormData object as the body. Append the file with formData.append('file', fileInput.files[0]) and pass the FormData to fetch without setting Content-Type β the browser sets it automatically with the correct multipart boundary. Do not set Content-Type manually or you'll break the boundary.
Yes. The global fetch function was added natively to Node.js in version 18 (stable in v21+). For older Node versions, you can install the node-fetch package or use Axios. In modern full-stack projects, the same fetch code runs in both browser and Node environments.