fetch starts a network request and returns a promise for a Response. Receiving HTTP 404 or 500 normally fulfills that promise: a server responded, but not with the success your application wanted. A network-level failure or an aborted request can reject it. Check the status before treating a response body as successful data.
The following module uses an injected fetch function so that the whole example runs without an internet service. Dependency injection simply means accepting the operation as an argument rather than hardcoding it.
async function loadTitle(url, { fetcher = fetch, signal } = {}) {
const response = await fetcher(url, { signal });
if (!response.ok) throw new Error(`Request failed (${response.status}).`);
const value = await response.json();
if (typeof value !== "object" || value === null || typeof value.title !== "string") {
throw new Error("Invalid title response.");
}
return value.title;
}
const fakeFetch = async (_url, { signal }) => {
signal?.throwIfAborted();
return new Response(JSON.stringify({ title: "Study JavaScript" }), {
status: 200, headers: { "content-type": "application/json" },
});
};
console.log(await loadTitle("/api/title", { fetcher: fakeFetch }));
const controller = new AbortController();
controller.abort();
try {
await loadTitle("/api/title", { fetcher: fakeFetch, signal: controller.signal });
} catch (error) {
console.log(error.name);
}
Running it prints Study JavaScript and AbortError. The fake checks an already-aborted signal; a real fetch implementation handles cancellation while its network operation is pending. The validator checks only the response shape this program consumes. A successful status and JSON content type would still not prove that the fields have correct types.
Cancellation is not rollback
An AbortController supplies a signal; calling abort asks participating operations to stop. A signal is single-use: after aborting, create a new controller for the next request. To impose a timeout, arrange for a timer to abort the controller and clear that timer in finally. A client stopping its wait does not prove that a server-side write did not happen. Retrying writes may require an idempotency key and server-side deduplication.
A search UI can also receive old responses after a newer search finishes. Abort the previous request where possible and associate each request with a generation number. Update the screen only if the completing request is still current. This is a correctness rule, not merely a loading-animation detail. Keep existing results during a refresh when that helps the user, and show a retry action for a failed current request.
Respect browser boundaries
Browsers enforce CORS for cross-origin reads. Setting mode: "no-cors" does not grant access to protected response content. Cookies and bearer credentials require deliberate configuration and a server that authorizes each operation. Never place a private API key in downloadable frontend code. Do not automatically retry every failed request: distinguish validation failures, unauthorized requests, rate limits and transient server errors.
Exercise: replace fakeFetch with fakes returning status 503, malformed JSON, and a JSON object whose title is numeric. Assert that none is reported as a successful title.
Solution guidance: status 503 should fail before parsing; invalid JSON fails parsing; the numeric title fails runtime validation. Use these separate tests to locate which contract broke instead of reporting every problem as "offline."
Reference: Using Fetch.
Previous: Promises, async/await and the Event Loop. Next: DOM, Events and Accessible Interaction.

