Loading data without reloading the page has become the foundation of every modern interface. For a long time this went through an aging object with obscure settings. A single function replaced it.
It fits in one call, but it hides a trap that catches everyone once: a failing response is not a failure as far as it is concerned.
Definition
fetch() sends an HTTP request and returns a Promise that settles with a Response object. That object carries the status code and the headers, but not the body yet: reading it requires a second wait.
async function loadProfile(id) {
const response = await fetch("/api/profiles/" + id);
if (!response.ok) {
throw new Error("Status " + response.status);
}
return await response.json(); // second wait: the body
}The ok property is true for any status between 200 and 299. The body is read with json(), text(), blob() or formData(), depending on what the server sent back.
What fetch calls a failure
The table below separates, across two columns, the situations that reject the promise from those that fulfill it despite a disappointing response.
| Situation | How the promise behaves |
|---|---|
| A 404 or 500 response | Fulfilled, with ok set to false |
| A redirect that was followed | Fulfilled, on the final response |
| Server unreachable | Rejected, with a TypeError |
| Request refused by the browser | Rejected, with no readable detail |
| Request canceled | Rejected, with the name AbortError |
That first row is the trap: a catch wrapped around a call will never see a 404. Testing ok is not optional.
Sending data
A write needs three settings: the method, the header that announces the format, and the serialized body.
const response = await fetch("/api/invoices", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: 49, currency: "EUR" }),
signal: AbortSignal.timeout(8000),
});The body has to be a string, which is what JSON produces through stringify. Passing the object directly would send the string [object Object], a silent mistake that is hard to spot.
fetch does not give up on its own after a while. A maximum delay is set with a signal, produced either by AbortSignal.timeout() or by an AbortController when the cancellation has to come from the interface.
Frequently asked questions
Why is my response body empty on the second read?
Because a response body is a stream, and a stream can only be read once. A second json() on the same object throws. If two parts of your code need the content, store the result in a variable, or duplicate the response with clone() before the first read.
Are cookies sent along with the request?
To the same origin, yes. To another domain, no, unless the credentials option is set to include, and the server then has to allow it explicitly. This is the most common point of friction between an interface and an API hosted elsewhere, described in detail under CORS.
Do I still need a library to call an API?
Not really for a simple need: the function ships in every current browser and in Node.js with nothing to install. A library still earns its place for interceptors, automatic retries or upload progress. That choice, and the error-handling layer around it, is covered in the JavaScript course.