Before this:Components & state
Fetching data from the front end
Key takeaways A modern page pulls fresh data without reloading by making HTTP requests from JavaScript with the browser’s fetch API. Requests are asynchronous — they take time and return a promise — so you handle them with async/await. Data usually travels as JSON, which you parse from the response. Every real fetch has three states you must design for — loading, error, and success — and, crucially, fetch does not throw on a 404 or 500, so you check the status yourself. The result flows into component state and the UI re-renders.
So far the data driving a component came from props or a click. In a real app most of it comes from a server — a list of decoded calls, a user profile, search results. This lesson is how the browser goes and gets that data over HTTP and folds it into the UI, all without the full-page reload that older sites needed. It is the bridge between the front-end mental model and the back-end half of this module.
fetch: an HTTP request from JavaScript
The browser gives you fetch, a function that makes an HTTP request from your code. You pass a URL; it returns a promise that resolves to a response object. Reading the body — here, parsing it as JSON — is a second asynchronous step. The cleanest way to write it is async/await, which lets asynchronous code read top to bottom:
async function getCalls() {
const response = await fetch("/api/calls"); // send the request, wait for headers
const data = await response.json(); // read + parse the JSON body
return data; // a normal JavaScript array/object
}
fetch is asynchronous because a network request takes real time — often much longer
than anything happening locally, as treating a remote call
taught. await pauses this function until the response is ready without freezing the
page; the browser stays responsive while the request is in flight. Under the hood
await is just friendlier syntax over promises and their .then() chains.
JSON: the data format on the wire
Data almost always crosses the network as JSON — JavaScript Object Notation — a
plain-text format of objects, arrays, strings, numbers, and booleans that both sides
understand. It maps so cleanly onto JavaScript values that response.json() turns the
text straight into usable objects.
{
"calls": [
{ "id": 1, "talkgroup": "Fire-1", "seconds": 12 },
{ "id": 2, "talkgroup": "EMS-3", "seconds": 4 }
]
}
To send data — say, saving a setting — you go the other way: set the method and headers, and put a JSON string in the body.
await fetch("/api/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme: "dark" }),
});
The Content-Type header tells the server the body is JSON, and JSON.stringify turns
your object into the text that rides in the request. This is the same request shape the
REST API on the back end is built to receive.
fetch doesn’t throw on HTTP errors
Here is the gotcha that catches everyone. fetch only rejects on a network failure —
no connection, DNS error, request never completes. An HTTP error status like 404 Not
Found or 500 Internal Server Error is, to fetch, a perfectly successful
round-trip: the server answered. So you must check the status yourself with
response.ok (true for 2xx) and handle a bad one:
async function getCalls() {
const response = await fetch("/api/calls");
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`); // 404, 500, ...
}
return response.json();
}
Skip this check and a 500 sails silently past try/catch, and your code tries to read
JSON out of an error page. Reading the status code — 2xx
worked, 4xx your request, 5xx the server — is the whole job here.
Loading, error, success: three states
A request is not instant, and it can fail, so a fetch is never just “the data.” It is three states the UI has to represent: loading while it’s in flight, error if it fails, and success with the data. Design for all three or the page looks broken the moment the network is slow. In a component, each is a piece of state:
function Calls() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
getCalls().then(setData).catch(setError); // runs after first render
}, []);
if (error) return <p>Couldn't load calls.</p>; // error state
if (!data) return <p>Loading…</p>; // loading state
return <ul>{data.calls.map((c) => <li key={c.id}>{c.talkgroup}</li>)}</ul>;
}
The useEffect hook runs the fetch after the component first renders — you don’t
fetch during render itself — and each resolved result drops into state, re-rendering the
UI. This loading-error-success shape is so universal that libraries like React Query and
SWR exist to manage it, plus caching and retries, so you don’t hand-write it every time.
Where fetch fits in the bigger picture
Fetching is the front end’s half of a conversation whose other half is the back end. The
browser calls an endpoint like /api/calls; a HTTP server
routes it to a handler, which reads a database and
returns JSON. Everything you send and receive rides on
HTTP and, in production, over
HTTPS so the data is encrypted in transit. Fetching
is also what makes a single-page app feel live — swapping data in place instead of
reloading, a rendering style we compare in
SSR vs. SPA vs. static.
Quick check: your fetch gets a 500 response but your try/catch never fires. Why?
Recap
- A page pulls fresh data without reloading using the browser’s fetch API, which returns a promise you handle with async/await.
- Requests are asynchronous because the network takes time;
awaitpauses your function without freezing the page. - Data travels as JSON — parse it with
response.json(), and send it withJSON.stringifyplus aContent-Type: application/jsonheader. - fetch does not throw on 404 or 500 — check
response.ok(or the status code) and handle HTTP errors yourself. - Every real fetch has loading, error, and success states; each is a piece of component state that re-renders the UI.
- Fetching is the front-end half of a conversation with a back-end API over HTTP.
Next up: Build tools & bundlers.
Frequently asked questions
What is fetch?
fetch is the browser’s built-in function for making HTTP requests from JavaScript. You give it a URL (and options like method and body), and it returns a promise that resolves to a response object. It’s how a page asks a back-end API for data without reloading — the modern replacement for the older XMLHttpRequest.
Why do I have to call response.json()?
Because the response arrives as a stream of bytes, and reading the body is itself asynchronous. fetch resolves as soon as the headers are in; response.json() reads the rest of the body and parses it from JSON text into a JavaScript object. It returns a promise, so you await it too.
Does a 404 or 500 make fetch throw an error?
No — and this trips up almost everyone. fetch only rejects on a network failure (no connection, DNS error). An HTTP error like 404 or 500 is a successful round-trip as far as fetch is concerned, so you must check response.ok yourself and handle the bad status.