Show the person exactly what is happening with a request.
What to build
One load(url) function that handles a success, an error response and a network failure.
Requirements
- While loading:
#statussaysLoading..., and all three buttons are disabled. - On success: list the products in
#products, asNAME: PRICE, and set#statustoLoaded N products. Loading again must not duplicate the list. - If the response is not OK: empty the list and set
#statustoCould not load (STATUS). - If the request itself fails, so that the promise rejects: empty the list and set
#statustoCould not reach the server. - The buttons are always turned back on afterwards.
Constraints
Do not change the first script, which stands in for a server.
Example
The button #bad requests /api/missing, which gives a status of 404, so #status shows Could not load (404).
Why it works
The two failures reach you differently: one as a normal response with ok set to false, and the other as a rejected promise. Treating them separately lets the message be accurate, and finally handles the part that is the same for all outcomes.
Hints
Hint 1
Write one load(url) function and call it from all three buttons.
Hint 2
Inside it, check response.ok and throw an Error that carries the status. Keep the network failure separate.
Hint 3
Use finally to turn the buttons back on.
Solution
Show one correct solution (try it yourself first)
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Loading states</title>
</head>
<body>
<button id="good">Load products</button>
<button id="bad">Load a missing page</button>
<button id="offline">Try while offline</button>
<p id="status" role="status">Nothing loaded yet.</p>
<ul id="products"></ul>
<script>
// A stand-in for a real server, because this page has no network.
function fakeFetch(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url === "/api/offline") {
reject(new TypeError("Failed to fetch"));
} else if (url === "/api/products") {
resolve({ ok: true, status: 200, json: async () => [{ name: "Pen", price: 10 }, { name: "Ruler", price: 25 }] });
} else {
resolve({ ok: false, status: 404, json: async () => ({}) });
}
}, 40);
});
}
</script>
<script>
const status = document.querySelector("#status");
const list = document.querySelector("#products");
const buttons = document.querySelectorAll("button");
function setBusy(busy) {
for (const button of buttons) {
button.disabled = busy;
}
}
async function load(url) {
status.textContent = "Loading...";
setBusy(true);
list.replaceChildren();
try {
const response = await fakeFetch(url);
if (!response.ok) {
status.textContent = "Could not load (" + response.status + ")";
return;
}
const products = await response.json();
for (const product of products) {
const li = document.createElement("li");
li.textContent = product.name + ": " + product.price;
list.append(li);
}
status.textContent = "Loaded " + products.length + " products";
} catch (err) {
status.textContent = "Could not reach the server";
} finally {
setBusy(false);
}
}
document.querySelector("#good").addEventListener("click", () => load("/api/products"));
document.querySelector("#bad").addEventListener("click", () => load("/api/missing"));
document.querySelector("#offline").addEventListener("click", () => load("/api/offline"));
</script>
</body>
</html>