Network requests take time. JavaScript doesn't wait and freeze the page — fetch() returns a Promise immediately, and your code continues when the response arrives. async functions with await let you write this step-by-step, like normal code.
fetch only rejects on network failure. An HTTP error such as 404 still resolves, so always check response.ok before reading response.json().
Good interfaces show three states: loading, success and error (with a retry). Run independent requests in parallel with Promise.all. This example fetches a students.json file served from the same folder.
[
{ "id": 1, "name": "Amina Hassan", "form": 4, "average": 86.0 },
{ "id": 2, "name": "Baraka Mushi", "form": 4, "average": 54.0 },
{ "id": 3, "name": "Neema Kimaro", "form": 4, "average": 75.0 },
{ "id": 4, "name": "Rehema Mollel", "form": 3, "average": 71.3 }
]<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Results</title>
<script src="app.js" defer></script>
</head>
<body>
<h1>Form results</h1>
<button id="load">Load results</button>
<p id="status" aria-live="polite"></p>
<ol id="results"></ol>
</body>
</html>const statusEl = document.querySelector("#status");
const list = document.querySelector("#results");
const loadBtn = document.querySelector("#load");
async function getJSON(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Request failed: HTTP ${response.status}`);
return response.json();
}
function render(students) {
const sorted = students.toSorted((a, b) => b.average - a.average);
list.replaceChildren(
...sorted.map((s) => {
const li = document.createElement("li");
li.textContent = `${s.name} (Form ${s.form}) - ${s.average.toFixed(1)}`;
return li;
}),
);
}
async function load() {
statusEl.textContent = "Loading...";
loadBtn.disabled = true;
try {
const students = await getJSON("students.json");
render(students);
statusEl.textContent = `Loaded ${students.length} students.`;
} catch (err) {
statusEl.textContent = `Could not load results (${err.message}). Try again.`;
} finally {
loadBtn.disabled = false;
}
}
loadBtn.addEventListener("click", load);
// Parallel requests: both start at once
async function loadEverything() {
const [students, missing] = await Promise.allSettled([getJSON("students.json"), getJSON("missing.json")]);
console.log(students.status, missing.status, missing.reason?.message);
}
loadEverything();Key points
awaitpauses only the async function — the page stays responsive.- Check
response.ok; fetch doesn't throw on 404/500. - Always show loading and error states, and re-enable controls in
finally.
Exercise
Add a search box that filters the loaded list as you type, and a "Form" dropdown filter. Then point the fetch at a wrong file name and check your error message and retry button work.
Show solution
Try the exercise yourself first — then compare your approach with this one.
Keep the loaded students in a variable and re-render whenever the search text or form filter changes — no new request needed. On failure, the status shows the error and a Retry button that simply calls load() again.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Results</title>
<script src="app.js" defer></script>
</head>
<body>
<input id="search" placeholder="Search names..." />
<select id="form-filter">
<option value="">All forms</option>
<option value="3">Form 3</option>
<option value="4">Form 4</option>
</select>
<p id="status" aria-live="polite"></p>
<ol id="results"></ol>
</body>
</html>const DATA_URL = "students.json"; // change to "studentz.json" to test the error state
const statusEl = document.querySelector("#status");
const list = document.querySelector("#results");
const search = document.querySelector("#search");
const formFilter = document.querySelector("#form-filter");
let students = [];
function render() {
const term = search.value.trim().toLowerCase();
const visible = students
.filter((s) => s.name.toLowerCase().includes(term))
.filter((s) => !formFilter.value || s.form === Number(formFilter.value));
list.replaceChildren(
...visible.map((s) => {
const li = document.createElement("li");
li.textContent = `${s.name} (Form ${s.form}) - ${s.average.toFixed(1)}`;
return li;
}),
);
statusEl.textContent = `Showing ${visible.length} of ${students.length} students.`;
}
async function load() {
statusEl.textContent = "Loading...";
try {
const response = await fetch(DATA_URL);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
students = await response.json();
render();
} catch (err) {
statusEl.textContent = `Could not load results (${err.message}). `;
const retry = document.createElement("button");
retry.textContent = "Retry";
retry.addEventListener("click", load);
statusEl.append(retry);
}
}
search.addEventListener("input", render);
formFilter.addEventListener("change", render);
load();