XHR (XMLHttpRequest) — встроенный в браузер объект JavaScript, который отправляет HTTP-запрос на сервер и получает ответ без перезагрузки страницы. По умолчанию запрос асинхронный: метод send() сразу возвращает управление, а ответ приходит позже в обработчик события load. Ниже — рабочий пример целиком, разбор open/send/onload, состояния readyState, обработка ошибок и сравнение с fetch.
Содержание
- Три термина, которые часто путают
- Стенд для примеров
- Минимальный рабочий пример: GET-запрос
- Ошибка новичка: читать ответ сразу после send()
- readyState: пять состояний запроса
- Обработка ошибок: что ловит onload, а что onerror
- POST: отправка JSON на сервер
- Вывод данных на страницу без XSS
- XHR и Promise: обертка для async/await
- Синхронный XHR: почему не стоит
- XHR или fetch
- Если не работает
- Выводы
- Где применяется / связь с практикой
- FAQ
Примеры проверены 24.09.2026 в Chromium 152 на локальном стенде (Node.js), код стенда — в первом разделе.
Три термина, которые часто путают
| Термин | Что это | Пример |
|---|---|---|
| AJAX | Подход: страница обменивается данными с сервером в фоне и обновляет только часть себя | Подгрузка комментариев без перезагрузки |
| XHR | Конкретный браузерный API для такого обмена, объект XMLHttpRequest |
new XMLHttpRequest() |
| fetch | Более новый браузерный API для того же, основан на Promise | fetch('/api/users') |
AJAX можно сделать и через XHR, и через fetch. Слово XML в названии историческое: XHR передает любой текст, JSON, двоичные данные (Blob, ArrayBuffer) и FormData.
Асинхронность здесь означает одно конкретное свойство: пока идет сетевой запрос, основной поток не ждет, страница продолжает реагировать на клики и прокрутку. Обработчик ответа выполнится позже, когда ответ придет и до него дойдет очередь в цикле событий.
Стенд для примеров
XHR не работает со страницы, открытой как файл (file://): нужен HTTP-сервер. Минимальный сервер на Node.js без зависимостей:
// server.js - запуск: node server.js, затем открыть http://localhost:8765/
const http = require('http');
const users = [{ id: 1, name: 'Анна' }, { id: 2, name: 'Борис' }];
http.createServer((req, res) => {
const json = (code, data) => {
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(typeof data === 'string' ? data : JSON.stringify(data));
};
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end('<!doctype html><meta charset="utf-8"><ul id="list"></ul>');
}
if (req.url === '/api/users' && req.method === 'GET') return json(200, users);
if (req.url === '/api/users' && req.method === 'POST') {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
let data;
try { data = JSON.parse(body); } catch { return json(400, { error: 'bad json' }); }
if (!data || typeof data.name !== 'string') return json(400, { error: 'name required' });
json(201, { ...data, id: 3 });
});
return;
}
if (req.url === '/api/slow') return setTimeout(() => json(200, { ok: true }), 3000);
if (req.url === '/api/broken') return json(200, '{oops');
json(404, { error: 'not found' });
}).listen(8765);
Код ниже вставляется в консоль разработчика (F12) на странице http://localhost:8765/.
Минимальный рабочий пример: GET-запрос
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.responseType = 'json';
xhr.timeout = 5000;
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('Пользователи:', xhr.response.map(u => u.name).join(', '));
} else {
console.log('Ошибка HTTP:', xhr.status);
}
};
xhr.onerror = () => console.log('Сетевая ошибка: ответа нет');
xhr.ontimeout = () => console.log('Сервер не ответил за 5 секунд');
xhr.send();
console.log('send() вернул управление, ответа еще нет');
В консоли две строки, и порядок важен — это и есть асинхронность:
send() вернул управление, ответа еще нет
Пользователи: Анна, Борис
Разбор по шагам:
new XMLHttpRequest()создает объект запроса. Библиотеки не нужны, конструктор есть во всех современных браузерах. В Node.js его нет — там используютfetch.open(method, url)только настраивает запрос: метод и адрес. Соединение он не открывает.responseType = 'json'просит браузер самому разобрать тело как JSON, вxhr.responseокажется готовый объект. Без этого тело лежит строкой вxhr.responseText, и нуженJSON.parse.timeout— лимит ожидания в миллисекундах, 0 означает «без лимита».- Обработчики
onload,onerror,ontimeoutназначаются доsend(). send()отправляет запрос. У GET тела нет, поэтому аргумент не передают (или передаютnull).
Полная сигнатура: open(method, url, async, user, password). Обязательны первые два аргумента. async по умолчанию true; false делает запрос синхронным (об этом ниже). user и password — для HTTP-аутентификации, в учебном коде их не используют.
Ошибка новичка: читать ответ сразу после send()
Неверно:
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.send();
console.log('responseText:', JSON.stringify(xhr.responseText));
console.log('readyState:', xhr.readyState);
Фактический результат — пустая строка, ответа еще нет:
responseText: ""
readyState: 1
Исправление: все, что зависит от ответа, переносится в onload (как в минимальном примере) или в Promise-обертку, показанную ниже.
readyState: пять состояний запроса
| readyState | Константа | Что произошло |
|---|---|---|
| 0 | UNSENT |
Объект создан, open() еще не вызван |
| 1 | OPENED |
Вызван open() |
| 2 | HEADERS_RECEIVED |
Пришли статус и заголовки ответа |
| 3 | LOADING |
Идет загрузка тела |
| 4 | DONE |
Запрос завершен: успешно, с ошибкой, по таймауту или отменен |
const xhr = new XMLHttpRequest();
console.log('после new: readyState =', xhr.readyState);
xhr.onreadystatechange = () => {
console.log('readyState =', xhr.readyState, 'status =', xhr.status);
};
xhr.open('GET', '/api/users');
xhr.send();
после new: readyState = 0
readyState = 1 status = 0
readyState = 2 status = 200
readyState = 3 status = 200
readyState = 4 status = 200
Для большого ответа состояние 3 может прийти несколько раз. В старом коде часто встречается onreadystatechange с проверкой readyState === 4; в новом проще onload, onerror, ontimeout — каждое событие отвечает за свой исход. Важная граница: readyState === 4 не означает успех, это просто «запрос закончился».
Обработка ошибок: что ловит onload, а что onerror
Частая ошибка — считать, что при 404 сработает onerror. Неверно:
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/userz');
xhr.responseType = 'json';
xhr.onload = () => {
console.log('onload, status =', xhr.status);
console.log(xhr.response.map(u => u.name));
};
xhr.onerror = () => console.log('onerror');
xhr.send();
Фактический результат: сработал onload, а затем код упал, потому что вместо массива пришел объект с описанием ошибки (строку браузера Failed to load resource ... 404 из вкладки сети здесь опускаем):
onload, status = 404
Uncaught TypeError: xhr.response.map is not a function
Исправление — проверять xhr.status внутри onload, как в минимальном примере. Сводка исходов (проверено на стенде):
| Ситуация | Событие | xhr.status |
xhr.response при responseType = 'json' |
|---|---|---|---|
| 200, корректный JSON | load |
200 | объект |
| Ответ 404 (так же ведут себя 500 и другие коды) | load |
404 | тело ответа сервера |
200, но тело не JSON (/api/broken) |
load |
200 | null |
| Сервер недоступен или запрет CORS | error |
0 | null |
Истек timeout |
timeout |
0 | null |
Вызван abort() |
abort |
0 | null |
Правило: HTTP-ошибка — это ответ, он приходит в load; error означает, что ответа не было вообще. Событие loadend срабатывает после любого из исходов, в нем удобно снимать индикатор загрузки.
Отмена и таймаут на медленном адресе:
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/slow');
xhr.onabort = () => console.log('abort, status =', xhr.status);
xhr.onload = () => console.log('onload не вызовется');
xhr.send();
setTimeout(() => xhr.abort(), 500);
abort, status = 0
abort() нужен, когда ответ стал неактуален: пользователь ушел со вкладки или ввел новый поисковый запрос.
POST: отправка JSON на сервер
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/users');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.responseType = 'json';
xhr.onload = () => console.log(xhr.status, JSON.stringify(xhr.response));
xhr.send(JSON.stringify({ name: 'Вера', job: 'QA' }));
201 {"name":"Вера","job":"QA","id":3}
setRequestHeader вызывается после open() и до send(). Объект нужно превратить в строку через JSON.stringify: если передать его напрямую, отправится строка [object Object]. Для HTML-формы и файлов удобнее send(new FormData(form)) — заголовок Content-Type с границей частей браузер поставит сам.
Вывод данных на страницу без XSS
Данные с сервера нельзя вставлять через innerHTML: имя пользователя вида <img src=x onerror=...> выполнит чужой код. Безопасный вариант — textContent:
const list = document.getElementById('list');
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.responseType = 'json';
xhr.onload = () => {
if (xhr.status !== 200 || !Array.isArray(xhr.response)) return;
for (const user of xhr.response) {
const li = document.createElement('li');
li.textContent = user.name;
list.append(li);
}
console.log(list.children.length, 'элемента в списке');
};
xhr.send();
На странице появятся пункты «Анна» и «Борис», в консоли — 2 элемента в списке. Проверка Array.isArray защищает от ответа неожиданной формы. Это закрывает только вывод: проверку прав и валидацию входа делает сервер.
XHR и Promise: обертка для async/await
function request(method, url, body) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.responseType = 'json';
xhr.timeout = 5000;
if (body !== undefined) {
xhr.setRequestHeader('Content-Type', 'application/json');
}
xhr.onload = () => {
if (xhr.status < 200 || xhr.status >= 300) {
reject(new Error('HTTP ' + xhr.status));
} else if (xhr.response === null && xhr.status !== 204) {
reject(new Error('Ответ не является корректным JSON'));
} else {
resolve(xhr.response);
}
};
xhr.onerror = () => reject(new Error('Сетевая ошибка или CORS'));
xhr.ontimeout = () => reject(new Error('Таймаут 5 с'));
xhr.send(body === undefined ? null : JSON.stringify(body));
});
}
async function main() {
const users = await request('GET', '/api/users');
console.log('GET:', users.length, 'пользователя');
const created = await request('POST', '/api/users', { name: 'Вера' });
console.log('POST: id =', created.id);
for (const url of ['/api/userz', '/api/broken']) {
try {
await request('GET', url);
} catch (e) {
console.log(url, '->', e.message);
}
}
}
main();
GET: 2 пользователя
POST: id = 3
/api/userz -> HTTP 404
/api/broken -> Ответ не является корректным JSON
Граница обертки: при responseType = 'json' браузер отдает null и для битого JSON, и для пустого тела, и для валидного JSON-литерала null, поэтому проверка xhr.response === null отличить их не может. Если сервер законно отвечает null или пустым телом с кодом 200, читайте тело как текст (responseType = 'text') и разбирайте JSON.parse в try.
Синхронный XHR: почему не стоит
Вызов open('GET', url, false) делает запрос синхронным: send() блокирует поток, пока не придет ответ, и страница в это время не реагирует на действия пользователя. В основном потоке страницы такой режим объявлен устаревшим, браузер пишет предупреждение в консоль. Кроме того, для синхронного запроса из документа нельзя задать таймаут — Chromium 152 при попытке выбрасывает:
InvalidAccessError: Failed to set the 'timeout' property on 'XMLHttpRequest': Timeouts cannot be set for synchronous requests made from a document.
Синхронный XHR допустим разве что в Web Worker, где он не блокирует интерфейс; в остальных случаях — асинхронный режим.
XHR или fetch
| Критерий | XHR | fetch |
|---|---|---|
| Стиль | События (onload, onerror) |
Promise, удобно с async/await |
| Ответ 404/500 | Приходит в load, проверять status |
Promise выполняется, проверять res.ok |
| Таймаут | Свойство timeout |
AbortSignal.timeout(ms) |
| Отмена | xhr.abort() |
AbortController |
| Прогресс отправки файла | xhr.upload.onprogress |
Встроенного события нет |
| Где доступен | Браузер, Web Worker | Браузер, Worker, Node.js (без флага с 18, стабильный с 21) |
Тот же GET на fetch:
async function loadUsers() {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
}
loadUsers().then(users => console.log('fetch:', users.map(u => u.name).join(', ')));
fetch: Анна, Борис
Для нового кода обычно берут fetch. XHR остается в двух случаях: нужен прогресс загрузки файла на сервер (xhr.upload.onprogress) или поддерживается старый проект, где на XHR построены обертки и библиотеки (например, $.ajax в jQuery). Про jQuery-вариант — в статье AJAX-запросы с помощью jQuery.
Если не работает
- В консоли ошибка про CORS,
status = 0. Запрос идет на другой домен, порт или протокол, а сервер не прислалAccess-Control-Allow-Origin. Чинится на сервере, не в клиенте. Для отправки cookie на другой домен нужен ещеxhr.withCredentials = trueи разрешение сервера. xhr.responseравенnullпри статусе 200. СтоитresponseType = 'json', а сервер вернул не JSON. Посмотрите тело во вкладке Network.- Сервер получил
[object Object]. Объект не прошел черезJSON.stringify. setRequestHeaderвыбрасываетInvalidStateError. Метод вызван доopen()или послеsend().- Ничего не происходит. Страница открыта как
file://или обработчики назначены после ответа — проверьте адрес и порядок строк.
Выводы
- XHR (
XMLHttpRequest) — браузерный API для HTTP-запросов без перезагрузки страницы; AJAX — подход, XHR — один из способов его реализовать. - По умолчанию запрос асинхронный: ответ доступен только в обработчиках событий, а не сразу после
send(). loadсрабатывает и на 404/500, поэтомуxhr.statusпроверяется всегда;errorозначает, что ответа не было.- Для нового кода обычно выбирают
fetch, XHR нужен для прогресса загрузки файлов и в legacy-проектах.
Где применяется / связь с практикой
Освойте тему на практике
С XHR сталкиваются при поддержке существующих сайтов и админок, где запросы написаны до появления fetch, и при загрузке файлов с индикатором прогресса. Понимание событий, статусов и асинхронности нужно и для fetch, и для любых HTTP-клиентов. Работу с сетью, асинхронный JavaScript и DOM последовательно разбирают на курсе «JavaScript-разработчик. Базовый уровень». Попробовать формат можно на открытых уроках Otus.
FAQ
Можно ли отправить несколько запросов одним объектом XHR?
Повторный вызов open() сбрасывает текущий запрос. Для параллельных запросов создают отдельный объект на каждый.
Как получить заголовки ответа?
xhr.getResponseHeader('Content-Type') возвращает один заголовок, xhr.getAllResponseHeaders() — все одной строкой. Для запросов на другой домен видны только заголовки, разрешенные сервером через CORS.
Работает ли XMLHttpRequest в Node.js?
Встроенного XMLHttpRequest в Node.js нет. На сервере используют встроенный fetch или модуль http.



