Chapter 9.4☕ 40 min read

Fetch API: Request, Response, Headers & AbortController

Fetch clean hai, lekin 404 pe REJECT nahi karta! response.ok check karo. AbortController se cancel karo.

01🧩 Partial Application — Bind & Beyond

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!
Fetch uses the browser's networking stack (not JS). The actual HTTP request happens on a separate OS thread. This is WHY fetch is non-blocking — the JS thread continues while the browser handles the network I/O on a separate thread.
02🔄 Currying — One Argument at a Time

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);
}
03⚡ Real-World Currying: Logger, Config, Validation

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!
📋 Always clean up your AbortController timeout with 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.
04🎯 Partial vs Curried — Jab Kya Use Karein?

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.");
  }
}
05🧹 Performance & Practical Tips

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 + "%");
  }
}
response.clone() creates a second tee'd ReadableStream — both streams receive the same data. This doubles memory usage for the body. Only clone when you truly need to read the body twice. For most use cases, read once with .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
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase