Fetch
A web page rarely needs to sit still. You might want to pull fresh data from a server, save a form, or check for new messages while the user keeps reading. JavaScript can do all of that by sending network requests in the background, then updating the page with whatever comes back — no full reload required.
Concrete things you reach for this feature to do:
- Submit an order,
- Load a user’s profile,
- Poll the server for the latest updates,
- …and plenty more.
The historical name for this pattern is AJAX — Asynchronous JavaScript And XML. The “XML” part is a fossil from the early 2000s, when servers often replied with XML. These days the payload is almost always JSON, but the acronym stuck. If you see “AJAX” in older articles or job listings, it just means “JavaScript talking to a server without reloading.”
There are several APIs for making these requests. fetch() is the modern one: promise-based, flexible, and supported across every current browser. Very old browsers lack it, but a polyfill fills the gap when you need one.
The shape of a fetch call
let promise = fetch(url, [options])
url— the address you want to hit.options— an optional settings object: HTTP method, headers, body, and more.
Leave options out and you get a plain GET request that downloads whatever lives at url. The browser fires the request immediately and hands you back a promise. Your code waits on that promise to find out what happened.
The part that trips people up: getting the full result is a two-stage process, and each stage is its own promise.
Stage one: the response arrives
The promise from fetch resolves with a Response object the moment the server replies with headers.
At this point the response line and headers are in — but the body may still be traveling over the wire. You can already inspect the HTTP status and read headers; you just can’t touch the body content yet.
One rule catches everyone at least once: a bad HTTP status is not an error to fetch. The promise rejects only when the request itself fails to complete — no network, DNS failure, a server that never answers. A 404 Not Found or 500 Internal Server Error counts as a successful round trip. The server answered; the answer was just unhappy. So you check the status yourself.
Two properties do that:
status— the numeric HTTP code, e.g.200.ok— a boolean,truewhenstatusfalls in the 200–299 range.
let response = await fetch(url);
if (response.ok) { // status is 200-299
// body-reading method covered just below
let json = await response.json();
} else {
alert("HTTP-Error: " + response.status);
}
You can feel this rule out for yourself. A Response object doesn’t have to come from the network — you can construct one directly, which is perfect for seeing how status drives ok. Type any HTTP code and watch which branch your code would take:
Stage two: reading the body
Once you have the Response, a second method call pulls in and decodes the body. Response exposes several promise-returning methods, each interpreting the bytes differently:
response.text()— read the body as a plain text string,response.json()— parse the body as JSON and hand back the resulting value,response.formData()— return aFormDataobject (covered in the next chapter),response.blob()— return a Blob: binary data tagged with a MIME-type,response.arrayBuffer()— return an ArrayBuffer: the low-level byte representation,- and
response.bodyis a ReadableStream, letting you consume the body chunk by chunk (there’s an example of streaming in a later chapter).
Here’s a real request. This pulls a list of recent commits from a public GitHub API and parses the JSON:
let url = 'https://api.github.com/repos/example/example-repo/commits';
let response = await fetch(url);
let commits = await response.json(); // read the body, parse it as JSON
alert(commits[0].author.login);
The same thing with plain promise chaining instead of await:
fetch('https://api.github.com/repos/example/example-repo/commits')
.then(response => response.json())
.then(commits => alert(commits[0].author.login));
Notice how response.json() returns a promise, so the .then chain has two steps: one for the headers, one for the parsed body. That mirrors the two awaits exactly.
Want the raw text instead of parsed JSON? Swap in response.text():
let response = await fetch('https://api.github.com/repos/example/example-repo/commits');
let text = await response.text(); // read the body as one big string
alert(text.slice(0, 80) + '...');
For a binary example, here’s fetching an image and displaying it. response.blob() gives you a Blob, and URL.createObjectURL turns that into a URL an <img> can load (see the Blob chapter for the details):
let response = await fetch('/article/fetch/logo-fetch.svg');
let blob = await response.blob(); // download as a Blob object
// build an <img> to hold it
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);
// point it at the blob
img.src = URL.createObjectURL(blob);
setTimeout(() => { // remove it after three seconds
img.remove();
URL.revokeObjectURL(img.src); // release the blob URL
}, 3000);
(stream, unread)
drains the stream
nothing left
Here’s that one-shot rule made real. The first button reads the body twice and catches the failure; the second calls clone() first, so both reads succeed. Same Response objects fetch would hand you:
Response headers
The response headers live in response.headers, a Map-like object. It isn’t a real Map, but it behaves like one: look up a header by name, or iterate over every header.
let response = await fetch('https://api.github.com/repos/example/example-repo/commits');
// grab a single header
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8
// walk through all of them
for (let [key, value] of response.headers) {
alert(`${key} = ${value}`);
}
Header names here are case-insensitive, so get('Content-Type') and get('content-type') return the same value.
Try it below. The response carries three headers; look any of them up in any casing, or list them all with a for..of loop:
Request headers
To attach headers to the outgoing request, pass a headers option — an object mapping header names to values:
let response = fetch(protectedUrl, {
headers: {
Authentication: 'secret'
}
});
There’s a catch. The browser keeps tight control over a set of forbidden request headers that your code is not allowed to set:
Accept-Charset,Accept-EncodingAccess-Control-Request-HeadersAccess-Control-Request-MethodConnectionContent-LengthCookie,Cookie2DateDNTExpectHostKeep-AliveOriginRefererTETrailerTransfer-EncodingUpgradeViaProxy-*Sec-*
These headers govern how HTTP itself behaves and how requests are routed and secured. Letting page scripts forge them would be a security and correctness hole, so the browser owns them exclusively. Try to set one and the browser quietly ignores it.
POST requests
For a POST — or any method other than GET — you flesh out the options object:
method— the HTTP method, e.g.POST,body— the request payload, one of:- a string (often JSON-encoded),
- a
FormDataobject, sent asmultipart/form-data, - a
BloborBufferSourcefor binary data, - a URLSearchParams object, for
x-www-form-urlencodeddata (rare).
JSON is what you’ll reach for most of the time. This example serializes a user object and posts it:
let user = {
name: 'Maya',
surname: 'Vance'
};
let response = await fetch('/article/fetch/post/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(user)
});
let result = await response.json();
alert(result.message);
Two details are doing quiet work here.
body has to be a string of bytes, not an object — that’s why JSON.stringify(user) wraps the object. And when body is a string, fetch defaults the Content-Type to text/plain;charset=UTF-8. Since we’re actually sending JSON, we override it with application/json so the server parses the payload correctly. Skip that header and many servers will misread the body.
{ name: ‘Maya’ }
‘{“name”:“Maya”}’
application/json
Sending an image
Binary payloads work the same way — hand fetch a Blob or BufferSource as the body. Here’s a <canvas> you can scribble on with the mouse; the “Submit” button ships the drawing to the server as a PNG:
<body style="margin:0">
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
<input type="button" value="Submit" onclick="submit()">
<script>
canvasElem.onmousemove = function(e) {
let ctx = canvasElem.getContext('2d');
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
};
async function submit() {
let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
let response = await fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
});
// the server replies with a confirmation and the image size
let result = await response.json();
alert(result.message);
}
</script>
</body>
There’s no Content-Type header in that fetch call, and that’s deliberate. A Blob already carries its own type — image/png here, because that’s what we passed to toBlob. For a Blob body, fetch uses that built-in type as the Content-Type automatically, so setting it by hand would just duplicate what the browser already knows.
Scribble on the canvas below and hit Export as Blob. We stop short of the network, but this is exactly the Blob you’d pass as the fetch body — note the type it carries on its own:
The same submit() written with promise chaining instead of async/await:
function submit() {
canvasElem.toBlob(function(blob) {
fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
})
.then(response => response.json())
.then(result => alert(JSON.stringify(result, null, 2)))
}, 'image/png');
}
Summary
A typical fetch is two awaits: one for the headers, one for the body.
let response = await fetch(url, options); // resolves with response headers
let result = await response.json(); // reads body as json
Or as a promise chain:
fetch(url, options)
.then(response => response.json())
.then(result => /* process result */)
Response properties worth knowing:
response.status— the HTTP status code,response.ok—truewhen the status is 200–299,response.headers— a Map-like object of the response headers.
Methods to read the body (pick exactly one per response):
response.text()— the body as text,response.json()— the body parsed as JSON,response.formData()— the body as aFormDataobject (multipart/form-data; see the next chapter),response.blob()— the body as a Blob (binary data with a type),response.arrayBuffer()— the body as an ArrayBuffer (low-level binary data).
Request options covered so far:
method— the HTTP method,headers— an object of request headers (with a browser-controlled forbidden list),body— the data to send: astring,FormData,BufferSource,Blob, orURLSearchParams.
The chapters ahead layer on more fetch options and real-world scenarios — tracking progress, aborting requests, and handling cross-origin calls.