Before this lesson
Understand promises, async/await, JSON and DOM updates.
What you will be able to do
Distinguish network failures from HTTP errors and model loading, success, empty and error states.
Understand the mechanism
A fulfilled fetch does not guarantee a successful HTTP status; 404 and 500 usually still resolve. Check response.ok before parsing, which can itself fail. Validate response shape and never treat server data as trusted DOM markup.
Clear old errors when starting, render data or an empty state on success, report failures clearly, and release the busy state on completion. Queries can finish out of order; abort older requests and use a request ID to prevent stale results replacing newer ones.
Cross-origin reads follow CORS rules; no-cors is not a workaround for readable JSON. This example uses a locally created Blob URL to avoid depending on an external service. Replace it with a same-origin API in a real app while keeping validation.
Runnable example
async function readCourses(url, signal) {
const response = await fetch(url, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (!Array.isArray(data) || !data.every(x => x && typeof x.title === "string")) {
throw new Error("Unexpected course data");
}
return data;
}
const url = URL.createObjectURL(new Blob([
JSON.stringify([{ title: "HTML" }, { title: "CSS" }])
], { type: "application/json" }));
const controller = new AbortController();
readCourses(url, controller.signal)
.then(items => console.table(items))
.catch(error => console.error(error.name, error.message))
.finally(() => URL.revokeObjectURL(url));
Trace and verify
Expect HTML and CSS. Replace the payload with {} to trigger schema failure or invalid JSON to trigger parsing failure. Abort before reading to get AbortError. For a real API, additionally test offline and HTTP error responses in Network tools.
Common mistakes and boundaries
Aborting a request does not roll back server work. Disabling a submit button alone cannot prevent duplicate writes. Explain failures in user-facing language while keeping diagnostic details in appropriate logs.
Practice challenge
Add a request ID so an older finally cannot clear the busy state of a newer request.
Reference solution and reasoning
Capture an incrementing current ID at start. Render or clear busy only if it is still the latest ID. Cancellation saves resources; identity checks protect UI correctness.
Lab: transport, HTTP and data success are separate
A fulfilled fetch does not mean a usable list arrived. An HTTP error, malformed JSON and a valid JSON value with the wrong shape fail at different stages. These in-memory Responses make the distinctions reproducible without relying on an external service.
Run this separate experiment in a modern browser Console; it is not a replacement for the main example above.
async function readCourses(response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
const rows = await response.json();
if (!Array.isArray(rows) || !rows.every(row =>
row !== null && typeof row === 'object' && typeof row.title === 'string')) {
throw new Error('Invalid course data');
}
return rows.map(row => row.title);
}
async function runCases() {
const cases = [
new Response('missing', {status: 404}),
new Response('<html>error</html>'),
new Response('{"title":"HTML"}'),
new Response('[{"title":"HTML"}]')
];
for (const response of cases) {
try { console.log(JSON.stringify(await readCourses(response))); }
catch (error) { console.log(error.name, error.message); }
}
}
runCases();Expected observations
The cases produce an HTTP 404 error, a SyntaxError, an Invalid course data error and ["HTML"]. SyntaxError wording is engine-dependent; test the error category rather than its exact message.
Why this happens and where it stops applying
A response body is normally consumed once. Calling text and then json on the same body fails; clone before consumption only when two readers are justified. Distinguish loading, empty, success and error in the UI: an empty array is a successful empty result. Repeated retries cannot repair a schema mismatch. Cancellation saves work, while a request-generation check separately prevents an obsolete completed result from replacing the latest selection.
Extension challenge
Should an empty title be valid? Predict the results for null, [], and [{"title":3}].
Reasoned solution
The current contract permits an empty string; require row.title.trim().length>0 if the product disallows it. null and numeric titles fail; [] succeeds. Define the contract before changing validation and tests instead of silently inventing missing fields.