Fetch & HTTP Requests
fetch(): The Modern HTTP Client
fetch is the built-in browser API for HTTP requests. It returns a promise that resolves to a Response object. Available in all modern browsers and Node.js 18+.
const response = await fetch("/api/users");
const users = await response.json();
GET Requests
GET is the default method. Build query strings with URLSearchParams.
const params = new URLSearchParams({ page: 2, limit: 20, sort: "name" });
const response = await fetch(`/api/users?${params}`);
const users = await response.json();
POST with JSON Body
Set Content-Type and stringify the body.
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice", email: "alice@example.com" })
});
const newUser = await response.json();
Other Methods
await fetch(`/api/users/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updatedUser)
});
await fetch(`/api/users/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role: "admin" })
});
await fetch(`/api/users/${id}`, { method: "DELETE" });
Headers
const response = await fetch("/api/data", {
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"Accept": "application/json"
}
});
// Reading response headers
console.log(response.headers.get("Content-Type"));
Error Handling: fetch Does Not Reject on HTTP Errors
This is the most important thing about fetch. A 404 or 500 is not a rejection. The promise rejects only on network failures. Always check response.ok.
async function fetchJSON(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
Comprehensive Error Handling
async function apiRequest(url, options = {}) {
let response;
try {
response = await fetch(url, options);
} catch (error) {
throw new Error(`Network error: ${error.message}`);
}
if (!response.ok) {
let body;
try { body = await response.json(); } catch { body = {}; }
const err = new Error(body.message || `HTTP ${response.status}`);
err.status = response.status;
throw err;
}
return response.json();
}
try {
const user = await apiRequest("/api/users/42");
} catch (error) {
if (error.status === 404) console.log("Not found");
else if (error.status === 401) redirectToLogin();
else console.error(error.message);
}
AbortController for Cancellation
Cancel in-flight requests. Essential for search-as-you-type, unmounting, and timeouts.
const controller = new AbortController();
const response = await fetch("/api/data", { signal: controller.signal });
controller.abort(); // cancels the request
Timeout Pattern
async function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
Search-As-You-Type
let currentController = null;
async function search(query) {
if (currentController) currentController.abort();
currentController = new AbortController();
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: currentController.signal
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
displayResults(await response.json());
} catch (error) {
if (error.name === "AbortError") return; // expected
displayError(error.message);
}
}
document.querySelector("#search").addEventListener("input", e => search(e.target.value));
Response Body Methods
The body is a stream consumed once. Each method can only be called once.
await response.json(); // parse as JSON
await response.text(); // plain text
await response.blob(); // binary blob
await response.arrayBuffer(); // raw bytes
await response.formData(); // form data
Streaming Large Responses
async function downloadWithProgress(url) {
const response = await fetch(url);
const total = parseInt(response.headers.get("Content-Length"), 10);
const reader = response.body.getReader();
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
received += value.length;
console.log(`${Math.round((received / total) * 100)}%`);
}
}
Sending Form Data & File Uploads
Do not set Content-Type for FormData — the browser sets it with the correct boundary.
const formData = new FormData(document.querySelector("#upload-form"));
formData.append("file", fileInput.files[0]);
const response = await fetch("/api/upload", {
method: "POST",
body: formData // no Content-Type header
});
When to Use fetch vs a Library
fetch is enough for most applications. Consider a library when you need interceptors, automatic retry, or request deduplication. Every one of those features can be built as a thin wrapper around fetch. Start with fetch. Add a wrapper when patterns repeat.
Common Pitfalls
Not checking response.ok. fetch does not reject on 404 or 500.
Setting Content-Type for FormData. The browser must set it to include the multipart boundary.
Reading the body twice. The body is a stream. Clone the response first if needed: response.clone().
Not cancelling stale requests. Search-as-you-type and navigation changes need AbortController.
Forgetting credentials: "include" for cross-origin cookies.
await fetch("https://api.other.com/data", { credentials: "include" });
Not encoding query parameters. Use URLSearchParams or encodeURIComponent.
Key Takeaways
fetchis the built-in HTTP client. No libraries needed for most use cases.- Always check
response.ok.fetchdoes not reject on HTTP error codes. - Set
Content-Type: application/jsonandJSON.stringify()the body for JSON requests. - Use
AbortControllerto cancel requests for search, timeouts, and cleanup. - Response bodies are streams: read once with
.json(),.text(), or.blob(). - Do not set
Content-Typewhen sendingFormData.