LEARN TO CODE | JAVASCRIPT | 04 APIS AND MINI PROJECTS
04.2 Fetch, JSON and error handling
Request JSON over HTTP, distinguish network and data failures, and render validated results as safe text.
01 | WHAT IS AN API?
An application programming interface , or API, is a defined way for programs to interact. A web API commonly exposes addresses that accept HTTP requests and return data. The address for a particular operation is often called an endpoint.
fetch is the browser API used here to make a request. The requested data may come from an API service or simply a JSON file served by your own local server.
We begin with supplied local files. That makes success, missing-file, malformed-JSON and wrong-shape outcomes reproducible without signing up for a service.
Use VS Code to edit index.html, script.js and style.css in one project folder. Link the external script with defer. Save changes and refresh the browser; inspect both the visible page and the Console.
The complete download has a separate folder for each project. Keep the examples independent to avoid colliding top-level variable names.
Serve the project over local HTTP for fetch and reliable storage testing. If your VS Code setup has a teacher-approved preview extension, use its preview command. With Live Server installed, right-click index.html and choose Open with Live Server. Keep the same hostname and port when testing saved data.
The example code runs in the browser and does not require Node.js. Opening a file URL is not the right workflow for the JSON fetch project.
02 | FOLLOW THE REQUEST AND RESPONSE
| Stage | What it gives you |
|---|---|
| fetch(url) | A promise for a Response |
| await fetch(url) | The Response object, when status/headers are available |
| response.ok | Whether the HTTP status is 200–299 |
| await response.json() | A value parsed from the response body |
| Validate the value | Confidence it has this app’s expected structure |
The Response object is not the final array. Reading and parsing its body is another asynchronous operation. That is why there are two awaits in the common pattern.
Response bodies are normally consumed once. Store the parsed value if you need to use it repeatedly rather than calling response.json() again on the same consumed body.
03 | START WITH A SMALL COMPLETE FUNCTION
async function readClubs() {
const response = await fetch("./clubs.json");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
}
async function showFirstRead() {
try {
const data = await readClubs();
console.log(data);
} catch (error) {
console.log(`Could not read clubs: ${error.message}`);
}
}
showFirstRead();
This introductory version assumes the downloaded clubs.json sits beside index.html. ./clubs.json is resolved against the document’s base URL, not the directory of an arbitrarily relocated script.
The later version validates the parsed data before displaying it. Keep this first function small enough to explain every step.
04 | HTTP FAILURE IS DIFFERENT FROM NETWORK FAILURE
A network failure or blocked request can reject fetch’s promise. But a server returning HTTP 404 has still returned a response: fetch normally fulfils with that Response.
Check response.ok explicitly and throw your own error when the HTTP status is unsuitable. Otherwise, you may attempt to parse an HTML error page as JSON and misdiagnose the underlying problem.
An empty but valid array is different again. It is a successful dataset containing no records, not necessarily an error.
05 | PREPARE THE TEST FILES
[
{
"name": "Robotics",
"capacity": 12
},
{
"name": "Art",
"capacity": 8
},
{
"name": "Coding",
"capacity": 16
}
]
Save the list above as clubs.json beside the HTML file. The download also includes empty.json containing [], broken.json containing an incomplete {, and wrong-shape.json containing an object instead of the expected array.
missing.json is deliberately not provided. Requesting it from a normal static preview server should produce an HTTP error. Some servers return a fallback HTML page with status 200 instead; inspect the Network panel to see the actual response.
06 | VALIDATE THE PARSED DATA
isClubList requires an array of at most 50 records. Every record must be a non-null object with a non-blank name up to 80 UTF-16 code units and an integer capacity from 1 to 200.
These rules belong to this example. JSON syntax alone does not establish them. A valid scalar, an array of numbers or a string capacity must be rejected before rendering.
The empty array passes validation. Array.every returns true for it because there is no element that violates the rule; the app then displays its deliberate empty-state message.
07 | COMPLETE LOADER WITH CLEANUP
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Load a club list from JSON</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<main>
<h1>Load a club list from JSON</h1>
<p>Choose a test response and load it. Serve this project over local HTTP.</p>
<p><label for="fetch-mode">Data to request : </label> <select id="fetch-mode"><option value="clubs">Valid club list</option><option value="empty">Empty club list</option><option value="broken">Malformed JSON</option><option value="wrong">Wrong data shape</option><option value="missing">Missing file: HTTP error</option></select></p>
<button id="fetch-load" type="button">Load clubs</button>
<p id="fetch-status" role="status" aria-live="polite">Nothing loaded yet.</p>
<ul id="fetch-list" aria-busy="false"></ul>
</main>
</body>
</html>
script.js
const dataPaths = {
clubs: "./clubs.json",
empty: "./empty.json",
broken: "./broken.json",
wrong: "./wrong-shape.json",
missing: "./missing.json"
};
const loadButton = document.querySelector("#fetch-load");
const modeInput = document.querySelector("#fetch-mode");
const status = document.querySelector("#fetch-status");
const list = document.querySelector("#fetch-list");
let loading = false;
function isClubList(value) {
return Array.isArray(value) && value.length <= 50 && value.every((club) =>
club !== null && typeof club === "object" && !Array.isArray(club)
&& typeof club.name === "string" && club.name.trim() !== "" && club.name.length <= 80
&& Number.isInteger(club.capacity) && club.capacity >= 1 && club.capacity <= 200
);
}
async function loadClubs() {
if (loading) return;
const url = dataPaths[modeInput.value];
if (url === undefined) return;
loading = true;
loadButton.disabled = true;
list.setAttribute("aria-busy", "true");
list.replaceChildren();
status.textContent = "Loading clubs...";
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: the data could not be loaded.`);
}
const clubs = await response.json();
if (!isClubList(clubs)) {
throw new Error("The returned data is not a valid club list.");
}
for (const club of clubs) {
const item = document.createElement("li");
item.textContent = `${club.name}: ${club.capacity} places`;
list.append(item);
}
status.textContent = clubs.length === 0 ? "Loaded successfully: no clubs in this list." : `Loaded ${clubs.length} clubs.`;
} catch (error) {
if (error.name === "AbortError") {
status.textContent = "The request took too long. Try again.";
} else if (error instanceof SyntaxError) {
status.textContent = "The response was not readable JSON.";
} else if (error instanceof TypeError) {
status.textContent = "The request could not be completed. Check the server, connection and browser Console.";
} else {
status.textContent = error.message;
}
} finally {
clearTimeout(timeout);
list.setAttribute("aria-busy", "false");
loadButton.disabled = false;
loading = false;
}
}
loadButton.addEventListener("click", loadClubs);
The download provides the full style.css used by these standalone projects. The live course pages inherit the site stylesheet.
08 | UNDERSTAND THE USER-INTERFACE STATES
The loading flag and disabled button prevent overlapping requests from this control. Clearing the old list avoids displaying stale results as though they came from the latest request.
AbortController supplies a signal to fetch. A timer calls abort after roughly eight seconds, cancelling an unfinished fetch or body read. finally clears the timer and restores the controls, whether success or failure occurred.
This is one timeout strategy for one active request. A larger search interface with repeated requests would also need a policy for cancellation and out-of-order results.
09 | TRY DATA SUCCESS AND FAILURE
This embedded preview fetches self-contained data URLs. Use the download for real HTTP-file and missing-file tests.
Nothing loaded yet.
This embedded activity fetches self-contained data URLs rather than contacting an external service. It demonstrates parsing, schema checks and rendering. For real HTTP status and missing-file tests, run the downloaded project over local HTTP.
TRY IT YOURSELF
Load valid data, then empty data, malformed JSON and the wrong shape. What distinguishes all four?
Reveal guidance
Valid records render a list. Empty data succeeds with a no-clubs message. Malformed JSON fails at parsing. Wrong-shaped valid JSON fails the app’s validation. In all cases the Load button becomes available again.
10 | CORS, SECRETS AND THE NETWORK PANEL
A different-origin API must allow your browser’s request through the appropriate CORS response policy. CORS is controlled by servers and browser enforcement; adding mode:"no-cors" does not make an unreadable response usable JSON.
Do not put secret API keys in browser JavaScript. Visitors can inspect downloaded source and requests. A service requiring a secret usually needs an appropriate server-side component.
The Network panel shows request URL, status and response content. Use it to distinguish a wrong path, an HTML fallback, a CORS problem and invalid JSON. A generic fetch TypeError alone does not reliably identify the exact cause.
11 | EXTEND WITHOUT SKIPPING CHECKS
TRY IT YOURSELF
After loading a validated list, display only clubs with capacity at least 12. Should filtering happen before or after validation?
Reveal guidance
Validate first, then filter the trusted shape and render Robotics and Coding. A filter does not replace validation of the whole received dataset under the chosen contract.
Next, build a quiz from an array of records. The first project keeps its data local so you can focus on interaction state; fetching a question bank is an optional later extension.
KEEP LEARNING
Previous page 04.3 Project: an interactive quiz
Download Section 04 example programs
Further reading: Promises · Fetch · JSON · Browser storage