Web pages often need structured data from an API before rendering account details, product rows, dashboard cards, or other dynamic content. The browser Fetch API returns a Response object first, so JSON loading should treat the HTTP response and the body parse as separate outcomes.

A small async helper keeps that boundary visible. It requests JSON with fetch(), rejects non-2xx responses with response.ok, checks for a JSON media type, and lets response.json() turn the body into a JavaScript value.

A same-origin profile.json file keeps the first request local while the browser still loads it through HTTP. Use the same helper with a real api/profile route when that route returns JSON and the page is allowed by the site's same-origin or CORS policy.

Steps to fetch JSON with JavaScript:

  1. Create a strict JSON response file for the local request.
    profile.json
    {
      "name": "Ada Lovelace",
      "role": "admin"
    }

    The payload must be strict JSON; comments, trailing commas, and malformed strings reach the catch branch when response.json() parses the body.
    Tool: JSON Validator

  2. Add a visible target and load the JavaScript module.
    index.html
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>Fetch JSON demo</title>
      </head>
      <body>
        <main>
          <h1>Profile</h1>
          <pre id="profile-output">Loading profile...</pre>
        </main>
        <script type="module" src="app.js"></script>
      </body>
    </html>
  3. Create the browser module that fetches and renders the JSON.
    app.js
    async function readJsonResponse(response) {
      if (!response.ok) {
        throw new Error(`Request failed with ${response.status}`);
      }
     
      const contentType = response.headers.get("content-type") || "";
      const isJson =
        contentType.includes("application/json") || contentType.includes("+json");
     
      if (!isJson) {
        throw new TypeError("Expected a JSON response");
      }
     
      return response.json();
    }
     
    async function fetchJson(url) {
      const response = await fetch(url, {
        headers: { Accept: "application/json" },
      });
     
      return readJsonResponse(response);
    }
     
    const output = document.querySelector("#profile-output");
     
    try {
      const profile = await fetchJson("profile.json");
      output.textContent = `${profile.name} (${profile.role})`;
    } catch (error) {
      output.textContent = `Could not load profile: ${error.message}`;
    }

    response.json() reads the response body once. If the app needs the raw body for error logging, read response.text() in that error path instead of parsing the same body twice.

  4. Start a local HTTP server from the directory that holds the three files.
    $ python3 -m http.server 8000
    Serving HTTP on :: port 8000 (http://[::]:8000/) ...

    Do not test the browser page from a file:// URL; local file loading and CORS checks do not match normal HTTP delivery.

  5. Open the page through the local server and confirm the parsed profile appears.
    http://localhost:8000/
    Ada Lovelace (admin)
  6. Save a smoke-test script for the helper's success, HTTP error, and malformed JSON branches.
    verify-fetch-json.mjs
    async function readJsonResponse(response) {
      if (!response.ok) {
        throw new Error(`Request failed with ${response.status}`);
      }
     
      const contentType = response.headers.get("content-type") || "";
      const isJson =
        contentType.includes("application/json") || contentType.includes("+json");
     
      if (!isJson) {
        throw new TypeError("Expected a JSON response");
      }
     
      return response.json();
    }
     
    async function fetchJson(url) {
      const response = await fetch(url, {
        headers: { Accept: "application/json" },
      });
     
      return readJsonResponse(response);
    }
     
    const profileUrl = `data:application/json,${encodeURIComponent(
      JSON.stringify({ name: "Ada Lovelace", role: "admin" }),
    )}`;
     
    const profile = await fetchJson(profileUrl);
    console.log(`Fetched profile: ${profile.name} (${profile.role})`);
     
    try {
      await readJsonResponse(
        new Response("Not found", {
          status: 404,
          headers: { "content-type": "application/json" },
        }),
      );
    } catch (error) {
      console.log(`HTTP error: ${error.message}`);
    }
     
    try {
      await readJsonResponse(
        new Response("{", {
          headers: { "content-type": "application/json" },
        }),
      );
    } catch (error) {
      console.log(`Broken JSON: ${error.name}`);
    }

    The data: URL keeps the success case local; the Response fixtures exercise the same response-checking function without depending on a third-party API.

  7. Run the smoke test.
    $ node verify-fetch-json.mjs
    Fetched profile: Ada Lovelace (admin)
    HTTP error: Request failed with 404
    Broken JSON: SyntaxError