# XMLHttpRequest (XHR)

> Learn how XMLHttpRequest works, why Fetch is the default for new code, and when XHR is still useful for upload progress.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2018-04-05 | Updated: 2026-07-18 | Topics: [Web Platform](https://flaviocopes.com/tags/platform/) | Canonical: https://flaviocopes.com/xhr/

<!-- TOC -->

- [Introduction](#introduction)
- [An example XHR request](#an-example-xhr-request)
- [Additional open() parameters](#additional-open-parameters)
- [onreadystatechange](#onreadystatechange)
- [Aborting an XHR request](#aborting-an-xhr-request)
- [Comparison with jQuery](#comparison-with-jquery)
- [Comparison with Fetch](#comparison-with-fetch)
- [Cross Domain Requests](#cross-domain-requests)
- [Uploading files using XHR](#uploading-files-using-xhr)

<!-- /TOC -->

## Introduction

[`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), usually called **XHR**, lets JavaScript make HTTP requests without reloading the page.

XHR is a legacy API, but it remains widely supported. The [Fetch API](https://flaviocopes.com/fetch-api/) is the better default for new code. XHR remains useful when you need upload progress events or maintain older code that already uses it.

XHR was invented at Microsoft in the 1990s. Apps like Gmail and Google Maps later showed what pages could do without full reloads.

Libraries like [jQuery](https://flaviocopes.com/jquery/) became popular partly because they hid early browser differences behind a simpler API.

## An example XHR request

The following code creates an XHR object and makes an asynchronous GET request:

```js
const xhr = new XMLHttpRequest()

xhr.addEventListener('load', () => {
  if (xhr.status >= 200 && xhr.status < 300) {
    console.log(xhr.responseText)
  } else {
    console.error(`HTTP error: ${xhr.status}`)
  }
})

xhr.addEventListener('error', () => {
  console.error('Network error')
})

xhr.open('GET', '/file.txt')
xhr.send()
```

## Additional open() parameters

In the example above, we passed the method and URL to `open()`.

XHR supports other HTTP methods, including POST, PUT, PATCH, and DELETE.

The third parameter controls whether the request is asynchronous:

```js
xhr.open('GET', '/file.txt', true)
```

It defaults to `true`. Do not set it to `false` in page code. [Synchronous XHR is deprecated](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Synchronous_and_Asynchronous_Requests#synchronous_request) because it blocks the main thread and can make the page freeze.

## onreadystatechange

Older XHR code often uses `readystatechange`. The event fires several times as the request moves through its states.

The states are

- 0 (UNSENT): `open()` has not been called
- 1 (OPENED): `open()` has been called
- 2 (HEADERS_RECEIVED): the HTTP headers have been received
- 3 (LOADING): the response begins to download
- 4 (DONE): the request has completed

For new XHR code, `load`, `error`, `abort`, and `timeout` events are usually clearer.

## Aborting an XHR request

An XHR request can be aborted by calling the `abort()` method on the `xhr` object.

## Comparison with jQuery

With jQuery these lines can be translated to:

```js
$.get('https://yoursite.com', (data) => {
  console.log(data)
}).fail((err) => {
  console.error(err)
})
```

## Comparison with Fetch

With the [Fetch API](https://flaviocopes.com/fetch-api/) this is the equivalent code:

```js
fetch('/file.txt')
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`)
    }
    return response.text()
  })
  .then((text) => console.log(text))
  .catch((error) => console.error(error))
```

## Cross Domain Requests

XHR follows the same-origin policy. To read a response from another origin, that server must allow your origin using [CORS (Cross-Origin Resource Sharing)](https://flaviocopes.com/cors/).

## Uploading files using XHR

XHR can report upload progress through its `upload` object. This is one reason it is still used:

```js
xhr.upload.addEventListener('progress', (event) => {
  if (event.lengthComputable) {
    const percent = Math.round((event.loaded / event.total) * 100)
    console.log(`${percent}% uploaded`)
  }
})
```

See the [MDN XHR progress-event guide](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_XMLHttpRequest#monitoring_progress) and my tutorial on [how to upload files using XHR](https://flaviocopes.com/file-upload-using-ajax/).
