AJAX with XMLHttpRequest and fetch
Every page so far has kept its data inside the script. Real sites often load data afterwards: search suggestions, the next batch of comments, a list that changes daily. AJAX is the old umbrella name for a page asking a server for data in the background and updating itself without a reload. The letters stand for Asynchronous JavaScript and XML. JSON has long since replaced XML as the usual format, but the name stuck.
This lesson does the same job twice. First comes XMLHttpRequest, the original API that you will still find in older code and countless tutorials. Then comes fetch, which is what new code uses.
The data and the page
JSON (JavaScript Object Notation) is text that looks like JavaScript arrays and objects, with stricter rules: keys and strings take double quotes, and comments or trailing commas are not allowed. Save this as parts.json:
[
{ "name": "Tower cooler", "qty": 1 },
{ "name": "Thermal paste", "qty": 1 },
{ "name": "Case fan", "qty": 2 }
]
Save this page next to it as index.html, in the same folder:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Parts list</title>
<script src="parts.js" defer></script>
</head>
<body>
<button type="button" id="load-xhr">Load with XMLHttpRequest</button>
<button type="button" id="load-fetch">Load with fetch</button>
<ul id="parts"></ul>
<p id="status"></p>
</body>
</html>
The three script blocks below go into parts.js, in order.
The same-origin rule, and why file pages fail
An origin is a scheme, a host and a port taken together: https://example.com and http://localhost:8000 are two different origins. A script may read responses from its own origin freely. Reading a response from another origin is blocked unless that server sends CORS headers (Cross-Origin Resource Sharing) that explicitly allow it, which public APIs built for browsers do. The rule exists so that a page you happen to visit cannot quietly read your webmail or your bank's pages while you are logged in.
Now double-click index.html. The address bar starts with file://, and the requests below fail with a CORS error in the console. Chrome and Firefox, like many browsers, treat every file opened from disk as an opaque origin, its own separate origin that matches nothing else. Even a JSON file in the same folder counts as "somewhere else". Some browsers have been more lenient about files in the same directory, but you cannot count on it.
There are two easy ways around this. One is a local web server, which puts every file in the folder on the same origin. If Python 3 is installed, run this inside the folder, then open http://localhost:8000/ in your browser:
python3 -m http.server 8000
The other is to skip the file and carry the JSON inside the URL itself, as a data URL:
const sample = [
{ name: 'Tower cooler', qty: 1 },
{ name: 'Thermal paste', qty: 1 },
{ name: 'Case fan', qty: 2 }
];
// With a local server, use: const source = 'parts.json';
const source = 'data:application/json,' + encodeURIComponent(JSON.stringify(sample));
JSON.stringify turns the array into JSON text, and encodeURIComponent escapes spaces, quotes and characters such as # that would otherwise break the URL. Chrome, Edge and Firefox accept a data URL with both of the APIs below.
XMLHttpRequest, step by step
const list = document.querySelector('#parts');
const statusLine = document.querySelector('#status');
function showParts(parts) {
list.textContent = '';
for (const part of parts) {
const item = document.createElement('li');
item.textContent = `${part.name} (${part.qty})`;
list.append(item);
}
}
document.querySelector('#load-xhr').addEventListener('click', () => {
const xhr = new XMLHttpRequest();
xhr.open('GET', source);
xhr.responseType = 'json';
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
showParts(xhr.response);
statusLine.textContent = 'Loaded with XMLHttpRequest.';
} else {
statusLine.textContent = `The server answered ${xhr.status}.`;
}
};
xhr.onerror = () => {
statusLine.textContent = 'The request failed before any answer arrived.';
};
xhr.send();
});
showParts is the for...of and createElement pattern from the loops lesson. The request itself takes six steps:
new XMLHttpRequest()creates a request object. Nothing is sent yet.open('GET', url)sets the method and the address. GET asks for data. POST sends data, which you pass tosend().responseType = 'json'asks the browser to parse the reply, soxhr.responsearrives as a ready-made array. Without it, you read text fromxhr.responseTextand callJSON.parseyourself.onloadruns when a response has arrived, whatever its status.onerrorruns when no response arrived at all: no network, a blocked cross-origin request, afile://page.send()starts the request and returns at once. The page stays usable while the request runs, which is the asynchronous part, andonloadfires later.
Step 4 catches people out. A missing file still produces a response, a 404 with a little error page, so onload runs and the status check is your job:
| Status | Meaning |
|---|---|
| 200 | OK, the body holds your data |
| 403 | Forbidden, the server refuses to hand it over |
| 404 | Not found, usually a wrong file name or path |
| 500 | Server error, something broke on the other end |
| 0 | No response at all, reported after a network error or a blocked request |
One more detail: if the file is not valid JSON, xhr.response comes back null. A trailing comma after the last item is the usual culprit.
The same request with fetch
async function loadWithFetch() {
try {
const response = await fetch(source);
if (!response.ok) {
statusLine.textContent = `The server answered ${response.status}.`;
return;
}
const parts = await response.json();
showParts(parts);
statusLine.textContent = 'Loaded with fetch.';
} catch (error) {
statusLine.textContent = `The request failed: ${error.message}`;
}
}
document.querySelector('#load-fetch').addEventListener('click', loadWithFetch);
fetch(url) starts a GET request and returns a promise, an object that stands for a result which is not ready yet. await pauses this function, not the page, until the promise settles, then hands you the value. You can only use await inside a function marked async, or at the top level of a module script. Otherwise this is an ordinary function in the sense of the functions lesson, passed by name to a listener.
The error handling lines up with what you saw above. Like onload, fetch treats a 404 or a 500 as a completed exchange, so you check response.ok, which is true for statuses 200 to 299. A network failure or a blocked request makes the promise reject instead, and await turns that into an exception for the catch block. response.json() reads and parses the body. It is asynchronous as well, hence the second await, and invalid JSON lands in catch too.
Side by side, fetch takes fewer lines, reads from top to bottom and fits with the rest of modern asynchronous JavaScript. XMLHttpRequest keeps one practical edge: upload progress events through xhr.upload, which fetch has no simple equivalent for.
Try it
Paste the page into the online HTML editor with the three script blocks inside a <script> element just before </body>, and keep the data URL version of source. The editor runs your page in a sandboxed frame where a relative path such as parts.json has nothing to load from, while a data URL carries its own content. Back on your local server, set source to 'nope.json', a file that does not exist: the first button reports 404 through onload, and the second reports it through response.ok.