XHR

In the evolution of web development, few technologies have had as profound an impact on user experience as XMLHttpRequest (XHR). As the fundamental building block of AJAX (Asynchronous JavaScript and XML), XHR revolutionized the way we interact with web applications by allowing browsers to exchange data with servers in the background. This capability enabled "partial page updates," where specific elements of a webpage could be refreshed without requiring a full page reload, paving the way for the highly interactive, single-page applications (SPAs) we use today.

While the modern Fetch API has become the preferred standard for most contemporary development due to its Promise-based architecture, XHR remains a vital tool in a developer's arsenal. Its ability to track upload progress, its robust support for legacy environments, and its straightforward cancellation mechanisms ensure it remains relevant in specific, high-performance, or maintenance-heavy scenarios.

The Mechanics of XHR

The XMLHttpRequest object is not a way to get XML; rather, it is a powerful API used to make HTTP requests. To use it, you must first instantiate the object using the XMLHttpRequest constructor. The lifecycle of a request is managed through a series of methods and properties that allow you to configure, execute, and monitor the communication.

Core Methods and Properties

To master XHR, one must understand its primary members:

  • open(method, url, async): This initializes the request. You specify the HTTP method (e.g., GET, POST), the target URL, and a boolean indicating whether the request should be asynchronous (which is almost always recommended).
  • setRequestHeader(name, value): This allows you to define custom HTTP headers. Crucially, this must be called after open() but before send().
  • send(body): This sends the request to the server. For GET requests, the body is typically null. For POST requests, this is where you pass your serialized data.
  • abort(): A vital method for stopping an ongoing request immediately.
  • timeout: A property used to set a maximum wait time (in milliseconds) before the request is automatically terminated.
  • readyState: An integer representing the current state of the request:
    • 0: Uninitialized
    • 1: Opened
    • 2: Headers received
    • 3: Loading (receiving response body)
    • 4: Completed
  • status: The numerical HTTP status code returned by the server (e.g., 200 for success, 404 for not found).
  • responseType: Defines the type of data you expect back (e.g., 'json', 'blob', 'arraybuffer'). Once set, the response property will automatically format the data accordingly.

Practical Implementation

Executing an Asynchronous GET Request

A typical GET request involves setting up the request, defining how to handle the response, and then sending it. It is important to note that the onload event triggers when the request is complete, but it does not guarantee that the HTTP status is successful. You must always verify the status code.

const xhr = new XMLHttpRequest();

// Initialize the request
xhr.open('GET', '/api/users?page=1', true);

// Configure response type and timeout
xhr.responseType = 'json';
xhr.timeout = 5000;

// Set necessary headers
xhr.setRequestHeader('Accept', 'application/json');

// Handle successful completion
xhr.onload = function () {
  if (xhr.status >= 200 && xhr.status < 300) {
    console.log('Data received:', xhr.response);
  } else {
    console.error('Server returned an error:', xhr.status, xhr.statusText);
  }
};

// Handle network-level errors
xhr.onerror = function () {
  console.error('A network error occurred.');
};

// Handle timeouts
xhr.ontimeout = function () {
  console.error('The request timed out.');
};

xhr.send();

Handling POST Requests and Data Serialization

When sending data to a server, such as during a login or a form submission, POST requests are used. There are two primary ways to handle this: sending JSON or using FormData.

1. Sending JSON Data
When sending JSON, you must manually stringify the object and set the Content-Type header so the server knows how to parse the incoming stream.

const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/login', true);
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');

xhr.onload = function () {
  if (xhr.status === 200) {
    console.log('Login successful');
  } else {
    console.error('Login failed');
  }
};

xhr.send(JSON.stringify({
  username: 'alice',
  password: 'secret'
}));

2. Sending Form Data (e.g., File Uploads)
The FormData object is incredibly useful for multipart/form-data requests. When using FormData, the browser automatically sets the correct Content-Type and boundary, so you do not need to call setRequestHeader for the content type.

const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('name', 'avatar');

const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload', true);
xhr.send(formData);

Advanced Features: Progress and Cancellation

One of the standout advantages of XHR over the standard Fetch API is its native, easy-to-use support for monitoring progress.

Monitoring Upload Progress

By accessing the xhr.upload object, you can listen to the progress event to provide real-time feedback to users, such as a progress bar during a large file upload.

xhr.upload.onprogress = function (event) {
  if (event.lengthComputable) {
    const percentComplete = (event.loaded / event.total) * 100;
    console.log(`Upload progress: ${percentComplete.toFixed(2)}%`);
  }
};

Canceling Requests

In modern web apps, users often navigate away from a page before a request finishes. To prevent unnecessary resource consumption or "race conditions" (where an old request updates the UI after a new one has already started), you can use the abort() method.

xhr.abort(); // Immediately terminates the request

Security, CORS, and Constraints

Because XHR can access data from different domains, it is strictly governed by the Same-Origin Policy. To allow cross-origin requests, the server must implement CORS (Cross-Origin Resource Sharing) by providing specific headers:

  • Access-Control-Allow-Origin: Specifies which domains can access the resource.
  • Access-Control-Allow-Methods: Lists the allowed HTTP methods.
  • Access-Control-Allow-Headers: Lists the allowed custom headers.

If your application needs to send cookies or authentication credentials with a cross-origin request, you must set xhr.withCredentials = true;. In this case, the server cannot use a wildcard (*) for Access-Control-Allow-Origin; it must specify the exact origin and include Access-Control-Allow-Credentials: true.

A Note on Synchronous Requests: While XHR allows for synchronous requests (by setting the third parameter of open() to false), this is strongly discouraged. Synchronous requests block the browser's main thread, freezing the entire UI until the request completes. Modern browsers have deprecated this behavior for several use cases to ensure a smooth user experience.

XHR vs. Fetch API: Which Should You Use?

Feature XMLHttpRequest (XHR) Fetch API
Syntax Event-based (Callback-heavy) Promise-based (Cleaner, modern)
Upload Progress Native support via xhr.upload Limited/Complex (requires streams)
Cancellation Simple abort() method Requires AbortController
Compatibility Excellent (Legacy browsers) Modern browsers (Requires polyfills for old ones)
Credentials withCredentials property credentials option in init object

Professional Best Practices

To ensure robust and maintainable code when working with XHR, follow these industry standards:

  1. Always Use Asynchronous Mode: Never block the main thread with synchronous requests.
  2. Implement Comprehensive Error Handling: Do not rely solely on onload. Always check status for HTTP errors, and implement onerror and ontimeout handlers to manage network failures and latency.
  3. Set Reasonable Timeouts: Prevent "hanging" requests by always defining a timeout value.
  4. Leverage responseType: Use the built-in parsing capabilities to handle JSON or binary data (Blobs) rather than manually converting strings.
  5. Encapsulate XHR: In modern projects, wrap XHR logic inside a Promise or a custom service layer. This allows you to use async/await syntax, making your code much more readable and easier to integrate with the rest of your application.
  6. Manage Race Conditions: Use abort() to cancel outdated requests when a user performs a new action that renders the previous request's data obsolete.