Fetch API: Request, Response, Headers & AbortController
Fetch clean hai, lekin 404 pe REJECT nahi karta! response.ok check karo. AbortController se cancel karo.
fetch(url, options) returns a Promise<Response>. Options include: method, headers, body, mode, credentials, cache, redirect, signal.
CRITICAL: fetch does NOT reject on HTTP errors (404, 500). The promise RESOLVES with response.ok = false.
Only rejects on network failure (no internet, DNS error, CORS block).
Always check: if (!response.ok) throw new Error(response.status).
Response properties: status (200, 404), statusText ("OK", "Not Found"), ok (true if 200-299), headers, url, redirected, type.
Body methods: .json(), .text(), .blob(), .arrayBuffer(), .formData() — ALL return Promises.
Body can only be read ONCE. Call .clone() first if you need to read it twice.
// ❌ THE TRAP: fetch doesn't reject on 404!
const response = await fetch("/api/users/999"); // User doesn't exist
// Promise RESOLVES! Not rejects!
console.log(response.ok); // false
console.log(response.status); // 404
// Your code continues unless you explicitly check!
// ✅ Always check response.ok
async function fetchUser(id) {
const response = await fetch("/api/users/" + id);
if (!response.ok) {
throw new Error("HTTP " + response.status + ": " + response.statusText);
}
return response.json();
}
// Usage with error handling
try {
const user = await fetchUser(1);
console.log(user);
} catch (err) {
console.error(err.message); // "HTTP 404: Not Found" or network error
}
// Body can only be read ONCE
const res = await fetch("/api/data");
const data1 = await res.json(); // Works!
const data2 = await res.json(); // TypeError: body stream already read!
// Fix: clone the response first
const clone = res.clone();
const data3 = await clone.json(); // Works on clone!
GET (default): No body. fetch(url) or fetch(url, { method: "GET" }).
POST: Send data with body. fetch(url, { method: "POST", headers, body }).
PUT: Replace entire resource. fetch(url, { method: "PUT", body }).
PATCH: Partial update. fetch(url, { method: "PATCH", body }).
DELETE: Usually no body. fetch(url, { method: "DELETE" }).
POST with JSON: Set Content-Type: application/json, body: JSON.stringify(data).
POST with FormData: Set body: formData — Content-Type is set AUTOMATICALLY with boundary.
Headers object: new Headers(), .get(), .set(), .has(), .append(), .delete().
// POST with JSON
async function createUser(userData) {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + getToken()
},
body: JSON.stringify(userData)
});
if (!response.ok) throw new Error("HTTP " + response.status);
return response.json();
}
// POST with FormData (file upload)
async function uploadFile(file, description) {
const formData = new FormData();
formData.append("file", file); // File from input
formData.append("description", description);
const response = await fetch("/api/upload", {
method: "POST",
body: formData // Don't set Content-Type! Browser sets it automatically!
});
return response.json();
}
// PUT — replace entire resource
async function replaceUser(id, userData) {
const response = await fetch("/api/users/" + id, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData)
});
return response.json();
}
// DELETE
async function deleteUser(id) {
const response = await fetch("/api/users/" + id, {
method: "DELETE",
headers: { "Authorization": "Bearer " + getToken() }
});
if (!response.ok) throw new Error("Delete failed: " + response.status);
}AbortController lets you cancel in-flight fetch requests.
const controller = new AbortController(); fetch(url, { signal: controller.signal }); controller.abort(); — cancels fetch, rejects with AbortError.
Timeout pattern: setTimeout(() => controller.abort(), 5000); — 5-second timeout.
abort() triggers a reject with DOMException named "AbortError".
Check: if (err.name === "AbortError") — distinguish abort from real errors.
One AbortController can abort MULTIPLE requests — pass same signal to multiple fetches.
After abort, controller is spent — create a new one for new requests.
// Cancel a fetch request
const controller = new AbortController();
fetch("/api/slow-endpoint", { signal: controller.signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === "AbortError") {
console.log("Request was cancelled!");
} else {
console.error("Fetch error:", err);
}
});
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
// ✅ Timeout helper function
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { ...options, signal: controller.signal })
.then(response => {
clearTimeout(timeoutId);
return response;
})
.catch(err => {
clearTimeout(timeoutId);
if (err.name === "AbortError") {
throw new Error("Request timed out after " + timeout + "ms");
}
throw err;
});
}
// Usage
const data = await fetchWithTimeout("/api/data", {}, 3000);
// Cancel multiple requests with one AbortController
const ctrl = new AbortController();
const p1 = fetch("/api/a", { signal: ctrl.signal });
const p2 = fetch("/api/b", { signal: ctrl.signal });
// ctrl.abort(); // Cancels BOTH requests!
clearTimeout() when the fetch succeeds. Otherwise the setTimeout keeps running even after the response is received, and calling abort() on an already-settled fetch is a no-op but wastes a timer.Same-origin = same protocol + host + port. Cross-origin = any difference.
Browser blocks cross-origin requests UNLESS server sends Access-Control-Allow-Origin header.
Simple requests (GET, POST with text/plain) may go through. Complex requests trigger a preflight OPTIONS request.
Preflight: Browser sends OPTIONS request first → server responds with allowed methods/headers → if approved, actual request is sent.
Common CORS headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials.
CORS is a BROWSER enforcement, not a server one — curl and Postman don't enforce CORS.
No-cors mode: fetch(url, { mode: "no-cors" }) — response is opaque (status 0, body empty, headers empty).
// CORS error example
// Frontend: https://myapp.com
fetch("https://api.other.com/data"); // Different origin!
// Browser blocks response if api.other.com doesn't send:
// Access-Control-Allow-Origin: https://myapp.com
// Console: "Access to fetch has been blocked by CORS policy"
// Preflight request for non-simple requests
fetch("https://api.other.com/data", {
method: "POST",
headers: { "Content-Type": "application/json" }, // Triggers preflight!
body: JSON.stringify({ name: "Sai" })
});
// Step 1: Browser sends OPTIONS request to api.other.com
// Step 2: Server must respond with:
// Access-Control-Allow-Origin: https://myapp.com
// Access-Control-Allow-Methods: POST
// Access-Control-Allow-Headers: Content-Type
// Step 3: If approved, browser sends the actual POST request
// Handling CORS in your fetch code
try {
const response = await fetch("https://api.other.com/data");
if (!response.ok) throw new Error("HTTP " + response.status);
const data = await response.json();
} catch (err) {
if (err.message.includes("CORS") || err instanceof TypeError) {
console.error("CORS error or network failure. Check server headers.");
}
}response.body is a ReadableStream — you can process data as chunks arrive instead of waiting for the full response.
const reader = response.body.getReader(); reader.read() returns { value: Uint8Array, done: boolean }.
Streaming is useful for large responses (file downloads, video, large JSON) — process without loading everything into memory.
TextDecoder converts Uint8Array chunks to strings.
ReadableStream also has .pipeTo(), .pipeThrough(), .tee() for stream composition.
Most of the time, .json() and .text() are sufficient — streaming is for special cases.
// Stream a large response
async function streamResponse(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let totalSize = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
// value is a Uint8Array chunk
totalSize += value.length;
const text = decoder.decode(value, { stream: true });
console.log("Received chunk:", text.length, "chars");
console.log("Total so far:", totalSize, "bytes");
}
console.log("Stream complete! Total:", totalSize, "bytes");
}
// Streaming with progress indicator
async function downloadWithProgress(url) {
const response = await fetch(url);
const contentLength = response.headers.get("Content-Length");
const total = parseInt(contentLength, 10);
const reader = response.body.getReader();
let received = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
received += value.length;
const percent = Math.round((received / total) * 100);
console.log("Progress:", percent + "%");
}
}
.json() or .text().Lo kar liya — Key Points:
- ✅ fetch() returns a Promise that RESOLVES (not rejects) even on HTTP errors like 404/500 — always check
response.ok - ✅ Body methods (.json(), .text(), .blob()) return Promises and can only be read ONCE — use .clone() to read twice
- ✅ POST with JSON requires Content-Type: application/json and body: JSON.stringify(data)
- ✅ POST with FormData should NOT set Content-Type manually — the browser sets it with the multipart boundary
- ✅ AbortController cancels in-flight fetch requests — pass signal option and call controller.abort()
- ✅ Distinguish AbortError (err.name === "AbortError") from real network errors in catch blocks
- ✅ CORS is a browser-enforced security mechanism — servers must send Access-Control-Allow-Origin header
- ✅ Complex cross-origin requests trigger a preflight OPTIONS request before the actual request
- ✅ response.body is a ReadableStream for processing large responses as chunks arrive
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login