Асинхронный запрос — это HTTP-запрос, который скрипт отправляет, не останавливая страницу: код идет дальше, а ответ сервера обрабатывается, когда придет. На этом построен AJAX — подход, при котором страница обновляет часть данных без полной перезагрузки. Данные при этом чаще всего передают в формате JSON, а сам запрос в современном JavaScript делают через fetch и async/await.
Содержание
- Четыре термина, которые путают
- Синхронно и асинхронно: порядок выполнения
- Минимальный рабочий пример: GET и POST с JSON
- Разбор функции requestJSON
- Ошибка 1: fetch не считает 404 и 500 ошибкой
- Ошибка 2: объект отправлен без JSON.stringify
- Несколько запросов: последовательно или параллельно
- Как это выглядит на странице
- fetch, XMLHttpRequest и jQuery.ajax
- JSON: что учесть при отправке
- Безопасность асинхронных запросов
- Если не получилось
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — рабочий пример GET и POST с JSON, проверка статуса ответа, таймаут и ошибки, на которых спотыкаются чаще всего. Примеры проверены в Node.js 24 (24.21.0); функция-клиент без изменений работает и в браузере.
Четыре термина, которые путают
| Термин | Что это | Пример |
|---|---|---|
| Асинхронный запрос | Способ выполнения: запрос не блокирует остальной код | Страница отвечает на клики, пока ждет сервер |
| AJAX | Подход: обновлять часть страницы данными с сервера без перезагрузки | Подгрузка комментариев, поиск с подсказками |
| JSON | Текстовый формат данных | {"id": 1, "title": "JavaScript"} |
| fetch, XMLHttpRequest | Программные интерфейсы браузера для HTTP-запросов | fetch('/api/courses') |
Буква X в AJAX исторически означает XML, но сейчас ответ почти всегда приходит в JSON. AJAX — не библиотека и не отдельная технология, а название приема.
Синхронно и асинхронно: порядок выполнения
При обычной отправке формы браузер уходит на сервер и загружает новую страницу целиком. При асинхронном запросе скрипт отправляет запрос, сразу продолжает работу, а обработчик ответа вызывается позже. Порядок хорошо видно на коротком примере, где ответ сервера изображает таймер:
console.log('1. Отправляем запрос');
setTimeout(() => console.log('3. Пришел ответ'), 0);
console.log('2. Страница продолжает работать');
1. Отправляем запрос
2. Страница продолжает работать
3. Пришел ответ
Даже с задержкой 0 мс обработчик выполняется после текущего кода. С сетевым запросом так же: результат нельзя получить «в следующей строке» без await или .then().
Минимальный рабочий пример: GET и POST с JSON
Файл demo.mjs поднимает учебный сервер на Node.js и делает к нему запросы. Сохраните его и запустите node demo.mjs (проверено на Node.js 24 LTS; глобальный fetch появился в Node.js 18 как экспериментальный и стал стабильным в 21, а ветки 18 и 20 уже без поддержки; порт 3000 должен быть свободен). Процесс завершится примерно через 3 секунды: столько ждет таймер медленного ответа.
import http from 'node:http';
// Учебный сервер: GET /api/courses, POST /api/orders, GET /api/slow
const server = http.createServer(async (req, res) => {
const send = (status, data) => {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(data));
};
if (req.method === 'GET' && req.url === '/api/courses') {
return send(200, [{ id: 1, title: 'JavaScript' }, { id: 2, title: 'Python' }]);
}
if (req.method === 'POST' && req.url === '/api/orders') {
if (!req.headers['content-type']?.startsWith('application/json')) {
return send(415, { error: 'Нужен Content-Type: application/json' });
}
let raw = '';
for await (const chunk of req) raw += chunk;
let order;
try { order = JSON.parse(raw); } catch { return send(400, { error: 'Невалидный JSON' }); }
if (!Number.isInteger(order?.courseId)) {
return send(400, { error: 'courseId должен быть целым числом' });
}
return send(201, { orderId: 42, courseId: order.courseId });
}
if (req.url === '/api/slow') {
return setTimeout(() => send(200, { ok: true }), 3000);
}
send(404, { error: 'Не найдено' });
});
// Клиент: одна функция на все запросы (так же работает в браузере)
async function requestJSON(url, { method = 'GET', body, timeoutMs = 5000 } = {}) {
const headers = { Accept: 'application/json' };
if (body !== undefined) headers['Content-Type'] = 'application/json';
const response = await fetch(url, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(timeoutMs),
});
const isJSON = response.headers.get('content-type')?.includes('application/json');
if (!response.ok) {
// тело ошибки может быть пустым или не JSON - статус важнее
const text = await response.text();
let detail = text;
if (isJSON) {
try { detail = JSON.parse(text)?.error ?? text; } catch { /* оставляем текст */ }
}
const error = new Error(`HTTP ${response.status}: ${detail}`);
error.status = response.status;
throw error;
}
return isJSON ? response.json() : response.text();
}
server.listen(3000, async () => {
const base = 'http://localhost:3000';
try {
console.log('GET:', await requestJSON(`${base}/api/courses`));
console.log('POST:', await requestJSON(`${base}/api/orders`, { method: 'POST', body: { courseId: 1 } }));
await requestJSON(`${base}/api/orders`, { method: 'POST', body: { courseId: 'один' } });
} catch (error) {
console.log('Ошибка:', error.status, error.message);
}
try {
await requestJSON(`${base}/api/slow`, { timeoutMs: 1000 });
} catch (error) {
console.log('Ошибка:', error.name, error.message);
}
server.close();
server.closeAllConnections();
});
В консоли появятся четыре строки: список курсов, созданный заказ, ошибка 400 от сервера и таймаут медленного запроса.
GET: [ { id: 1, title: 'JavaScript' }, { id: 2, title: 'Python' } ]
POST: { orderId: 42, courseId: 1 }
Ошибка: 400 HTTP 400: courseId должен быть целым числом
Ошибка: TimeoutError The operation was aborted due to timeout
Разбор функции requestJSON
fetch(url, options)сразу возвращает промис.awaitприостанавливает только эту async-функцию, а не всю страницу.- Промис от
fetchвыполняется, когда пришли статус и заголовки. Тело читается отдельно:response.json()тоже возвращает промис, поэтому нужен второйawait. - Для POST объект превращают в строку
JSON.stringify(body)и указываютContent-Type: application/json— так сервер понимает, как разбирать тело. response.okравноtrueдля статусов 200-299. Все остальное функция превращает в исключение со статусом, чтобы вызывающий код обработал его в одномcatch. Статус проверяется до разбора JSON: если прокси вернет 502 с пустым телом, вызывающий код получитHTTP 502, а не маскирующую его ошибкуSyntaxError: Unexpected end of JSON input.AbortSignal.timeout(5000)отменяет запрос, если ответа нет за 5 секунд. Без этого запрос к зависшему серверу может ждать очень долго.
Тип ответа проверяется по заголовку Content-Type: при ошибках сервер или прокси часто отдают HTML, а не JSON.
Ошибка 1: fetch не считает 404 и 500 ошибкой
Неверно — надеяться, что catch сработает на любом неудачном ответе:
const response = await fetch('http://localhost:3000/api/cours');
const data = await response.json();
fetch выполнился успешно, хотя адрес с опечаткой. Дальше два исхода, и оба плохие. Учебный сервер из примера отвечает на 404 JSON-ом, поэтому data молча станет { error: 'Не найдено' }, и код продолжит работать с ошибкой как с данными. Если же сервер или прокси отдает на 404 HTML-страницу, падает уже разбор тела:
SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON
Исправление — проверять response.ok (или response.status) до чтения данных, как в requestJSON. Промис fetch отклоняется только при сбое сети, отмене или ошибке CORS. Например, при недоступном сервере Node.js 24 выдает TypeError: fetch failed; в браузерах текст этой ошибки отличается.
Ошибка 2: объект отправлен без JSON.stringify
Неверно — передать объект в body напрямую:
const order = { courseId: 1 };
const request = new Request('http://localhost:3000/api/orders', { method: 'POST', body: order });
console.log(request.headers.get('content-type'));
console.log(await request.text());
text/plain;charset=UTF-8
[object Object]
Объект привелся к строке [object Object], а заголовок стал text/plain. Сервер из примера ответит 415. Исправление — body: JSON.stringify(order) и заголовок 'Content-Type': 'application/json'.
Несколько запросов: последовательно или параллельно
Если запросы не зависят друг от друга, не ждите их по очереди: Promise.all запускает все сразу и падает на первой ошибке, Promise.allSettled дожидается всех и сообщает статус каждого.
const results = await Promise.allSettled([
Promise.resolve({ id: 1 }),
Promise.reject(new Error('HTTP 503')),
]);
for (const r of results) console.log(r.status, r.value ?? r.reason.message);
fulfilled { id: 1 }
rejected HTTP 503
В реальном коде вместо Promise.resolve и Promise.reject стоят вызовы requestJSON(...). Последовательный await нужен, когда второй запрос использует результат первого.
Как это выглядит на странице
В браузере та же функция requestJSON вызывается из обработчика формы. Страница не перезагружается, результат выводится в блок:
<form id="order">
<input name="courseId" value="1">
<button>Записаться</button>
</form>
<p id="result"></p>
<script type="module">
// сюда вставьте функцию requestJSON из примера выше
const form = document.querySelector('#order');
const result = document.querySelector('#result');
form.addEventListener('submit', async (event) => {
event.preventDefault();
try {
const order = await requestJSON('/api/orders', {
method: 'POST',
body: { courseId: Number(form.courseId.value) },
});
result.textContent = `Заявка ${order.orderId} создана`;
} catch (error) {
result.textContent = `Не получилось: ${error.message}`;
}
});
</script>
event.preventDefault() отменяет обычную отправку формы. Данные с сервера выводятся через textContent, а не innerHTML, чтобы строка из ответа не выполнилась как разметка.
fetch, XMLHttpRequest и jQuery.ajax
| Критерий | fetch | XMLHttpRequest | jQuery.ajax |
|---|---|---|---|
| Стиль кода | Промисы, async/await | Обработчики событий | Колбэки и промисоподобный jqXHR |
| Ошибка на 404/500 | Нет, проверять response.ok |
Нет, проверять status |
Да, вызывается .fail |
| Прогресс загрузки файла | Нет простого способа | Есть upload.onprogress |
Через XHR |
| Нужна библиотека | Нет | Нет | Да |
Для нового кода обычно берут fetch. XMLHttpRequest остается в старых проектах и там, где нужен прогресс отправки файла. Как устроен jQuery-вариант и как переписать его на fetch — в статье jQuery AJAX: $.ajax, $.get, $.post.
JSON: что учесть при отправке
JSON.stringify не переносит все типы JavaScript: Date превращается в строку, undefined и функции в объекте пропадают, BigInt вызывает исключение. На сервере данные снова разбираются из текста, поэтому даты передают строкой ISO и явно преобразуют обратно. Подробнее о правилах формата и разборе — в статье JSON: создание и чтение.
Безопасность асинхронных запросов
- CORS — ограничение браузера на чтение ответов с другого домена, а не защита сервера: curl или скрипт вне браузера отправят запрос без него. Доступ проверяется на сервере.
- Данные от клиента сервер проверяет всегда, даже если форма проверяет их в браузере (как
Number.isIntegerв примере). - Если API работает с cookie сессии, запросы, меняющие данные, нужно защищать от CSRF: токен, атрибут
SameSiteу cookie, проверкаOrigin. - Ответ сервера вставляется на страницу через
textContentили экранирование, а не вinnerHTMLкак есть.
Если не получилось
SyntaxError: Unexpected token '<'— пришел HTML вместо JSON: неверный URL, 404 или страница ошибки. Посмотрите статус и тело во вкладке Network.- В консоли браузера ошибка про CORS — сервер другого домена не разрешил чтение ответа. Нужен заголовок
Access-Control-Allow-Originна сервере или запрос через свой бэкенд. - Вместо данных выводится
Promise { <pending> }— пропущенawaitпередfetchилиresponse.json(). - Сервер отвечает 400 или 415 — тело не в JSON или нет заголовка
Content-Type: application/json. TimeoutError— сервер не успел ответить: увеличьте таймаут или разберитесь, почему он медленный.
Выводы
- Асинхронный запрос не блокирует страницу; AJAX — прием обновления части страницы, JSON — формат данных, fetch — API для запроса.
fetchотклоняет промис только при сбое сети, отмене или CORS; статусы 4xx и 5xx проверяются черезresponse.ok.- Для POST с JSON нужны
JSON.stringifyи заголовокContent-Type: application/json. - Таймаут задается через
AbortSignal.timeout, независимые запросы запускаются параллельно черезPromise.allилиPromise.allSettled. - Данные с сервера выводятся через
textContent, проверка входных данных и прав остается на сервере.
Где применяется / связь с практикой
Освойте тему на практике
Асинхронные запросы есть почти в любом современном интерфейсе: формы без перезагрузки, фильтры каталога, бесконечная лента, чат, автосохранение. Работа с fetch, промисами и async/await входит в базу курса «JavaScript-разработчик. Базовый уровень». Разобрать отдельные темы с преподавателем можно на открытых уроках Otus.
FAQ
Можно ли сделать синхронный запрос в браузере?
XMLHttpRequest поддерживает синхронный режим (третий аргумент open равен false), но в основном потоке он устарел и замораживает страницу; у fetch синхронного режима нет.
Чем await отличается от .then()?
Это две записи одного механизма промисов: await читается как последовательный код и работает внутри async-функции или на верхнем уровне модуля, .then() — цепочка колбэков.
Как отменить запрос по кнопке, а не по таймауту?
Создать AbortController, передать controller.signal в fetch и вызвать controller.abort(); промис отклонится с ошибкой AbortError.



