AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Fetch API Deep-Dive: Making HTTP Requests

📚 Web APIs⏱️ 21 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 21 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Open the IRCTC "Live Train Status" page, or a live cricket score widget on a news site, and watch the numbers change every few seconds — the score updates, the train's location moves — but the page itself never flashes white and reloads the way it does when you click a normal link. Something is quietly talking to a server in the background, fetching fresh data, and slotting it into the page you're already looking at. That "something," in modern JavaScript, is almost always the Fetch API. This chapter is about exactly how that conversation between your browser and a server works, line by line, and how to write it correctly yourself.

Why Doesn't the Page Reload?

To understand fetch, you first have to be precise about what a normal webpage load actually is. When you type a URL and hit Enter, your browser sends a request to a server and gets back an entire HTML document as the response. The browser throws away whatever was on screen and paints the new document from scratch. That's a full page navigation, and it is expensive — every stylesheet, script, and image gets re-fetched and re-rendered.

A live score widget can't afford that. It only wants two numbers — say, the current runs and wickets — not an entire new page. So instead of navigating to a new URL, the JavaScript already running on the page sends its own small, private HTTP request in the background, using code, and asks only for the data it needs. Before 2015, this was done with an older, clunkier tool called XMLHttpRequest. The Fetch API is the modern, cleaner replacement for exactly this job: JavaScript code asking a server for data (or sending data to a server) without leaving the current page.

The Request-Response Cycle, Formalized

Every HTTP interaction, whether it's a full page load or a background fetch, has the same shape. A request leaves the client (your browser) carrying three things: a method that says what kind of action this is (most commonly GET, meaning "give me data," or POST, meaning "here is data, save it"), a URL that says where to send it, and optionally headers and a body carrying extra information or data to send. The server processes that request and sends back a response, which carries a status code (a three-digit number summarizing what happened), its own headers, and usually a body containing the actual data.

This is the entire mental model you need before touching any code. Fetch is simply a JavaScript function that constructs one of these requests, sends it, and hands you the response once it arrives. The hard part isn't the network — it's that "once it arrives" clause, because the response does not arrive instantly, and JavaScript refuses to sit around waiting for it.

Synchronous vs Asynchronous: A Bank Counter and a Drop Box

Picture two ways of submitting five loan applications at a bank. Method one: you stand at a single counter, hand over form 1, and the clerk works on it for three minutes while you wait, doing nothing else, before you can hand over form 2. Five forms at three minutes each costs you fifteen minutes of your own time, minute by minute, in order. This is synchronous execution — one task must fully finish before the next one can even begin.

Method two: you drop all five forms into a drop box, which takes about one minute total, and then go sit down and read a newspaper. A clerk calls your name individually as each application finishes processing, in whatever order they happen to get done. You were never blocked — you did other things while the slow part happened elsewhere. This is asynchronous execution, and it is exactly how fetch behaves.

JavaScript in a browser runs on a single thread — it can only execute one instruction at a time, like the bank clerk. If fetch() blocked that thread while waiting for a server reply that might take 300 milliseconds or 3 seconds, your entire webpage would freeze — no scrolling, no button clicks, nothing — until the network finished. So instead, fetch() hands the actual networking work off to the browser's own machinery, returns control to your code immediately, and promises to tell you later when the data is ready. That promise is, quite literally, called a Promise.

Enter fetch(): Your First Request, Traced Numerically

Here is the smallest possible example, followed by a precise trace of what happens and when:

console.log("1: Starting");
fetch("https://api.example.com/cricket/live-score");
console.log("2: This line runs before any data arrives");

Run this and the console prints "1: Starting" and then, almost instantly, "2: This line runs before any data arrives" — even though the fetch call is sitting right between them. At the exact moment fetch() is called (call this t = 0 ms), it does not pause to talk to the server itself. It creates a Promise object in the "pending" state, quietly starts the real network request in the background, and returns that pending Promise right away, letting line 3 run within a fraction of a millisecond. If the server takes, say, 340 milliseconds to respond, your code has already moved on and finished everything else it had to do 340 milliseconds before the data shows up. Nothing in that snippet ever uses the response — we've thrown away the Promise fetch returned, which is a real (if harmless) mistake. To actually use the data, you attach a handler using .then().

fetch("https://api.example.com/cricket/live-score")
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    console.log(data.team1 + " : " + data.team2);
  })
  .catch(function(error) {
    console.error("Network problem:", error.message);
  });

Trace it exactly. fetch(...) returns a Promise immediately, in the pending state. The first .then() registers a callback that will run only once that Promise settles — it does not run now. Execution then falls off the bottom of this block instantly, and the rest of your script (and the rest of the page) keeps running. Later — asynchronously, whenever the server actually responds — the browser resolves the original Promise with a Response object, and only then does the first callback fire, calling response.json(). That method itself returns another Promise (because reading and parsing the response body also takes a moment), which is why it needs its own .then() to receive the final JavaScript object as data. If anything goes wrong at the network level anywhere along this chain — the phone loses signal, the DNS lookup fails, the connection is refused — the Promise chain rejects instead of resolving, and control jumps straight to .catch(), skipping every .then() in between.

The Response Object: What You Actually Get Back

The value your first .then() receives is not the data itself — it's a Response object describing the HTTP response as a whole, before its body has even been fully read. It carries several properties worth knowing precisely:

  • response.status — the numeric status code, such as 200 or 404.
  • response.ok — a boolean shortcut that is true only when the status is in the 200–299 range, and false for everything else, including 404 and 500.
  • response.headers — the response headers, such as Content-Type, accessible via methods like response.headers.get("Content-Type").
  • response.json() — reads the body and parses it as JSON, returning a Promise that resolves to a JavaScript object or array.
  • response.text() — reads the body as a plain string instead, for when the server isn't sending JSON.

Notice that reading the body is a separate, asynchronous step from receiving the response headers. This two-stage design exists because a server might send a large body slowly, in chunks, and the browser hands you the Response as soon as the headers arrive rather than waiting for the entire payload to finish downloading first.

The Misconception That Breaks Everything: fetch() Does Not Reject on HTTP Errors

This is the single most common bug students write with the Fetch API, and CBSE-style questions love to probe it, so learn it precisely. Many learners assume that if a server responds with a "not found" page, fetch()'s Promise will reject, sending control into .catch(). It will not. As far as fetch is concerned, any completed HTTP exchange — a 200, a 404, even a 500 — counts as a successful round trip, because the browser did successfully send a request and receive a response; it's just that the response happens to describe an error. The Promise only rejects for genuine network failures: no internet connection, a DNS lookup failure, a CORS block, or the request being aborted. A 404 "Not Found" response resolves the Promise completely normally.

This means the naive version of our first example is silently broken:

fetch("https://api.example.com/cricket/live-score")
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    console.log(data.team1);
  })
  .catch(function(error) {
    console.error("Failed:", error.message);
  });

If that URL is mistyped and the server replies with a 404 page whose body is HTML like <h1>Not Found</h1>, this code does not go to .catch(). Instead, response.json() tries to parse that HTML text as JSON, fails, and that parsing failure is what eventually triggers .catch() — reporting a confusing "Unexpected token" error that has nothing obviously to do with a missing URL. The correct fix is to check response.ok yourself, immediately, before trying to parse anything:

fetch("https://api.example.com/cricket/live-score")
  .then(function(response) {
    if (!response.ok) {
      throw new Error("Server responded with status " + response.status);
    }
    return response.json();
  })
  .then(function(data) {
    console.log(data.team1 + " : " + data.team2);
  })
  .catch(function(error) {
    console.error("Could not load score:", error.message);
  });

Now a 404 is caught deliberately: response.ok is false, so we manually throw an error inside the .then() callback, which immediately rejects the chain and routes to .catch() with a message that actually tells you the real status code. Always check response.ok (or response.status) before trusting the body — this single habit is what separates working fetch code from code that looks fine until the server has a bad day.

async/await: The Same Chain, Written Straighter

Chains of .then() get hard to read once there are three or four steps. JavaScript offers a second syntax, async/await, that does exactly the same thing under the hood but reads top-to-bottom like ordinary synchronous code. Any function marked async can use await in front of a Promise to "pause" — only that function, never the rest of the page — until the Promise settles, then continue with the resolved value as an ordinary variable.

async function getLiveScore() {
  try {
    const response = await fetch("https://api.example.com/cricket/live-score");
    if (!response.ok) {
      throw new Error("Server responded with status " + response.status);
    }
    const data = await response.json();
    console.log(data.team1 + " : " + data.team2);
  } catch (error) {
    console.error("Could not load score:", error.message);
  }
}

getLiveScore();

Trace this the same way. When getLiveScore() is called, execution enters the function and hits await fetch(...). This is where the pause happens — but crucially, it is getLiveScore itself that pauses, not the browser tab. Any code after the call to getLiveScore(), and any button click elsewhere on the page, keeps working normally while this function waits. Once the response arrives, execution resumes exactly where it left off, response now holds the real Response object, and the function proceeds line by line — checking response.ok, then await-ing response.json() for the same reason as before: parsing the body is itself asynchronous. Any error thrown anywhere inside the try block, whether from a network failure or our own manual throw, is caught by the matching catch block — functionally identical to .catch() in the Promise-chain version, just written with familiar try/catch syntax.

Sending Data: POST Requests with a Body

Every example so far has been a GET — asking for data. To send data, say, registering a new student in a school portal, you pass a second argument to fetch(): an options object specifying the method, headers, and body.

async function registerStudent() {
  const response = await fetch("https://api.school.example/students", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      name: "Ananya Sharma",
      grade: 9,
      rollNumber: 21
    })
  });

  if (response.ok) {
    const result = await response.json();
    console.log("Registered with ID:", result.studentId);
  } else {
    console.log("Registration failed:", response.status);
  }
}

Three details matter here. First, method: "POST" tells the server this request is submitting data, not just retrieving it. Second, JSON.stringify(...) is required because the body of an HTTP request is always sent as raw text (or binary) over the wire — you cannot send a live JavaScript object directly, so it must be converted to a JSON-formatted string first. Third, the "Content-Type": "application/json" header is how you tell the server, in words, what format that string body is in, so it knows to parse it as JSON rather than, say, plain form data. Forgetting this header is a common source of servers silently misreading a perfectly correctly-stringified body.

HTTP Methods and Status Codes Worth Knowing Precisely

Fetch can send any standard HTTP method via the method option:

  • GET — retrieve data; the default if you omit method entirely; should never have a body.
  • POST — submit new data, such as creating a new record.
  • PUT — replace an existing resource entirely with new data.
  • PATCH — update part of an existing resource, leaving the rest untouched.
  • DELETE — remove a resource.

And status codes, grouped by their leading digit, which is itself meaningful:

  • 2xx (success): 200 OK for a normal successful response; 201 Created specifically after a successful POST that made a new resource.
  • 4xx (client's fault): 400 Bad Request when the request itself was malformed; 401 Unauthorized when you must log in; 404 Not Found when the URL doesn't correspond to anything.
  • 5xx (server's fault): 500 Internal Server Error, meaning the server itself broke while trying to handle an otherwise valid request.

Note precisely where the responsibility lines fall: a 4xx means the client (your fetch call) asked for something wrong or forbidden; a 5xx means the client's request was fine but the server failed anyway. This distinction is exactly what response.status lets you branch on.

A Necessary Note on Cross-Origin Requests

One thing that trips up nearly every beginner testing fetch for the first time: if your webpage is served from one address (say, a file opened locally, or a site on one domain) and you fetch() a URL on a completely different domain, the browser may block the response from ever reaching your JavaScript, even though the server sent it back successfully. This is a deliberate browser security feature called CORS (Cross-Origin Resource Sharing) — it exists so a malicious webpage can't silently read data from, say, your bank's servers using your logged-in session. The server on the other end has to explicitly opt in, via a response header, to allow your page's origin to read its replies. If a fetch call fails with a vague network-level error in the console mentioning "CORS," the problem is not a typo in your code — it's that the server hasn't granted permission, and the Promise rejects exactly the way a genuine network failure would.

The Full Cycle, Visualized

How fetch() talks to a server while your code keeps running Browser (Client) your JS code Server api.example.com 1. GET /live-score 2. 200 OK + JSON body Meanwhile, back in your code... SYNCHRONOUS PHASE — t = 0 ms 1 console.log('Starting') 2 fetch(url) -> Promise: pending 3 console.log('Runs next') fetch() returned instantly -- it did NOT pause the program. ASYNC CALLBACK — t ~ 340 ms 4 response object arrives .then(r => r.json()) 5 console.log('Data!', data) This callback runs only after the response is ready.

Check Your Understanding

  1. A student writes fetch("/api/marks").then(r => console.log(r)) and is confused that the console shows a Response object instead of their actual marks data. What step did they skip, and why is it a separate asynchronous step rather than instant?
  2. Your fetch request hits a URL that no longer exists, and the server correctly replies with status 404. Does the fetch Promise reject? Walk through exactly what happens if the code never checks response.ok and just calls response.json() directly.
  3. Trace the output order and approximate timing of this code, assuming the server takes 500 ms to respond: console.log("A"); fetch(url).then(function(r){ console.log("B"); }); console.log("C");
  4. Rewrite this .then() chain using async/await with a try/catch block: fetch(url).then(r => r.json()).then(d => console.log(d)).catch(e => console.log(e)).
  5. Why must JSON.stringify() be called on the data before putting it in the body of a POST request, and what header should accompany it?
  6. A fetch call to a different domain fails silently in the console with a CORS-related message, even though you're certain the server exists and responded. Is this a bug in your fetch code? Explain what's actually happening.

Summary

The Fetch API is JavaScript's tool for sending an HTTP request from code already running on a page, without triggering a full navigation. Calling fetch(url) returns a Promise immediately — pending at first — while the actual network exchange happens in the background, letting the rest of your synchronous code run without waiting. That Promise resolves to a Response object carrying the status code and headers; reading the body itself, via response.json() or response.text(), is a second asynchronous step. The single most important habit is checking response.ok before trusting the body, because fetch's Promise only rejects on genuine network failure — a 404 or 500 counts as a "successful" round trip as far as the Promise is concerned. async/await offers the same mechanics with synchronous-looking syntax, pausing only the function it's written in. Sending data outward uses the same function with a second options argument specifying method, headers, and a JSON-stringified body. Understanding this one request-response cycle — request out, response back, body parsed separately, errors checked explicitly — is the foundation every dynamic, non-reloading webpage you've ever used is built on.

Think About It

Think about this: How would you explain fetch api deep-dive: making http requests to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where fetch api deep-dive: making http requests is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting fetch api deep-dive: making http requests to at least 3 other topics you have studied.
← Event Delegation: Efficient DOM Event HandlingREST vs GraphQL: Understanding Modern APIs →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn