AJAX Requests
Learn how to make asynchronous HTTP requests using XMLHttpRequest (AJAX) — the original technique for fetching data from a server without reloading the page.
AJAX Requests
AJAX stands for Asynchronous JavaScript And XML. It is the technique of making HTTP requests from JavaScript without reloading the page. Despite the name, modern AJAX typically uses JSON instead of XML. In this episode you will learn how AJAX works using the XMLHttpRequest object.
What Is AJAX?
Before AJAX, getting new data from a server required a full page reload. AJAX changed everything — it lets you fetch data in the background and update parts of the page dynamically.
| Without AJAX | With AJAX |
|---|---|
| Click a link → entire page reloads | Click a button → data loads in the background |
| Server sends a full HTML page | Server sends only the data (JSON/XML) |
| Screen flashes white during reload | Page updates smoothly, no flicker |
| Slow, bandwidth-heavy | Fast, lightweight |
The XMLHttpRequest Object
XMLHttpRequest (XHR) is the original browser API for making AJAX requests. It has been available since the early 2000s and is supported in all browsers.
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
xhr.onload = function() {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.send();
Breaking Down the Steps
| Step | Code | What It Does |
|---|---|---|
| 1. Create | new XMLHttpRequest() | Creates a new request object |
| 2. Configure | xhr.open('GET', url) | Sets the HTTP method and URL (does not send yet) |
| 3. Handle response | xhr.onload = function() {} | Defines what happens when the response arrives |
| 4. Send | xhr.send() | Actually sends the request to the server |
XHR Ready States
Before onload was widely supported, developers used onreadystatechange which fires at each stage of the request:
xhr.onreadystatechange = function() {
console.log('Ready state:', xhr.readyState);
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
}
};
| readyState | Value | Meaning |
|---|---|---|
| UNSENT | 0 | Request created, open() not called yet |
| OPENED | 1 | open() has been called |
| HEADERS_RECEIVED | 2 | send() called, headers and status received |
| LOADING | 3 | Response body is being received |
| DONE | 4 | Request complete — response is fully received |
The modern approach uses onload which fires only when the request is fully complete (readyState 4), making the code cleaner.
Practical Example: Fetching a List of Posts
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts');
xhr.onload = function() {
if (xhr.status === 200) {
const posts = JSON.parse(xhr.responseText);
posts.forEach(post => {
console.log(post.id + ': ' + post.title);
});
} else {
console.log('Error: ' + xhr.status);
}
};
xhr.onerror = function() {
console.log('Network error occurred');
};
xhr.send();
Error Handling
| Event | When It Fires |
|---|---|
onload | Request completed (could be 200, 404, 500, etc.) |
onerror | Network-level failure (DNS error, no internet, CORS block) |
ontimeout | Request took too long (if timeout is set) |
Important: onload fires even for error HTTP status codes (404, 500). You must check xhr.status to know if the request was successful. onerror only fires for network-level failures where no HTTP response was received at all.
Sending POST Requests
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://jsonplaceholder.typicode.com/posts');
// Set the content type header for JSON data
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status === 201) {
const newPost = JSON.parse(xhr.responseText);
console.log('Created post:', newPost);
}
};
// Send JSON data in the request body
xhr.send(JSON.stringify({
title: 'My New Post',
body: 'This is the post content.',
userId: 1
}));
For POST requests, you pass the data as a string to xhr.send(). Use JSON.stringify() to convert a JavaScript object to a JSON string, and set the Content-Type header so the server knows what format to expect.
Creating a Reusable AJAX Function
function makeRequest(method, url, data, callback) {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
if (data) {
xhr.setRequestHeader('Content-Type', 'application/json');
}
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
callback(null, JSON.parse(xhr.responseText));
} else {
callback('Error: ' + xhr.status, null);
}
};
xhr.onerror = function() {
callback('Network error', null);
};
xhr.send(data ? JSON.stringify(data) : null);
}
// Usage:
makeRequest('GET', '/api/posts', null, function(err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
});
This wrapper function follows the Node.js error-first callback pattern — the first argument to the callback is an error (or null if successful), and the second is the data. This pattern will become important when we discuss callbacks in the next episode.
AJAX Is Asynchronous
console.log('Before request');
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
xhr.onload = function() {
console.log('Response received');
};
xhr.send();
console.log('After request');
// Output:
// Before request
// After request
// Response received
Just like setTimeout, the request is sent and JavaScript immediately moves on. The onload callback runs later when the server responds. This is the fundamental async pattern that the rest of this series builds on.
XHR vs Modern Alternatives
| Feature | XMLHttpRequest | Fetch API |
|---|---|---|
| Syntax | Verbose, event-based | Clean, Promise-based |
| Promises | No (callback-based) | Yes (built-in) |
| Streams | No | Yes |
| Cancel requests | xhr.abort() | AbortController |
| Browser support | All browsers | Modern browsers |
fetch() is the modern replacement for XMLHttpRequest, but understanding XHR first gives you a solid foundation for how async HTTP requests work under the hood.
Key Takeaways
- AJAX lets you fetch data from a server without reloading the page
XMLHttpRequestis created, configured withopen(), given a callback withonload, and fired withsend()onloadfires when the response is received — but you must checkxhr.statusfor successonerrorfires only on network-level failures, not HTTP error codes- POST requests require
setRequestHeader('Content-Type', 'application/json')andJSON.stringify() - AJAX requests are asynchronous — code after
xhr.send()runs immediately, before the response arrives - The Fetch API is the modern replacement, but understanding XHR builds your async fundamentals