Чтобы отправить HTTP-запрос из JavaScript, вызовите fetch(url, options). Функция сразу возвращает промис, а когда сервер ответит, промис выполнится объектом Response. Данные из него достают отдельным вызовом: await response.json() для JSON, response.text() для текста.
Ниже разберём GET и POST-запросы, заголовки, формы, обработку ошибок и таймаут. Каждый пример можно скопировать в консоль браузера или в файл для Node.js, под кодом показан его вывод. Если слово «API» пока звучит туманно, начните с разбора что такое API простыми словами.
Что такое fetch и что он возвращает

Fetch API — встроенный в браузер интерфейс для сетевых запросов. Он пришёл на смену XMLHttpRequest, на котором раньше строили AJAX: страница получает данные с сервера без перезагрузки. Устанавливать ничего не нужно, fetch доступен во всех современных браузерах.
В Node.js функция тоже глобальная. По документации Node.js флаг --experimental-fetch перестал быть нужен в версии 18.0.0, а в 21.0.0 функция перестала считаться экспериментальной.
Как устроен результат вызова:
fetch()возвращает промис, данных в нём сразу нет. Проверкаfetch(url) instanceof Promiseдаётtrue.- Промис выполняется объектом
Response, как только пришли заголовки ответа. Тело к этому моменту ещё не прочитано. - Тело читают методами
json(),text(),blob(), и каждый из них тоже возвращает промис.
Ответ сервера чаще всего приходит в формате JSON. Как он устроен, мы разбирали в статье что такое JSON, а из чего состоят сам запрос и ответ, рассказано в материале что такое HTTP.
GET-запрос через fetch: пошаговая инструкция
Для примеров возьмём JSONPlaceholder, бесплатный тестовый API. По его руководству созданные и изменённые записи на сервере на самом деле не сохраняются, а ответ только имитирует успех, так что экспериментировать можно смело.
- Вызовите
fetchс адресом ресурса. Метод по умолчанию GET, второй аргумент для него не нужен. - Дождитесь ответа через
awaitили.then(). - Проверьте
response.ok: он равенtrueпри статусах 200–299. - Прочитайте тело нужным методом, для JSON это
response.json(). - Оберните всё в
try…catch, чтобы поймать сбой сети.
Вариант с async и await
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
console.log(response.status, response.ok);
const todo = await response.json();
console.log(todo);
Вывод:
200 true
{ userId: 1, id: 1, title: 'delectus aut autem', completed: false }
Верхнеуровневый await работает в консоли браузера и в модулях (файл .mjs или type="module"). В обычном скрипте код нужно поместить внутрь async function.
Вариант с цепочкой then
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(todo => console.log(todo.title))
.catch(error => console.error('Ошибка сети:', error));
Вывод: delectus aut autem. Запись через then встречается в старом коде и в примерах на форумах. Для нового кода удобнее async/await: ошибки ловятся обычным try…catch, а код читается сверху вниз.
POST-запрос с JSON
Чтобы отправить данные, передайте вторым аргументом объект настроек. В нём нужны три поля: метод, заголовок с типом данных и само тело.
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Привет', body: 'Первый пост', userId: 1 }),
});
console.log(response.status, await response.json());
Вывод:
201 { title: 'Привет', body: 'Первый пост', userId: 1, id: 101 }
Статус 201 значит «создано», сервер вернул запись вместе с новым id. Так же отправляют PUT, PATCH и DELETE, меняется только method. Чем эти методы отличаются друг от друга, разобрано в статье методы HTTP: POST и GET, а почему повторный POST может создать дубль, объясняет материал об идемпотентности.
Объект в body без
JSON.stringifyне уйдёт как JSON. Fetch превратит его в строку[object Object], и сервер, который ждёт JSON, не сможет её разобрать.
Параметры запроса, формы и заголовки
Строка запроса через URLSearchParams
Параметры GET-запроса не склеивайте руками: кириллица и пробелы сломают адрес. URLSearchParams сам закодирует значения.
const params = new URLSearchParams({ userId: 1, completed: true });
const response = await fetch(`https://jsonplaceholder.typicode.com/todos?${params}`);
const todos = await response.json();
console.log(params.toString(), todos.length);
Вывод: userId=1&completed=true 11. Строка «курсы js» после кодирования выглядит так: q=%D0%BA%D1%83%D1%80%D1%81%D1%8B+js, и сервер получает её без искажений.
Отправка формы и файлов через FormData
const form = new FormData();
form.append('name', 'Вера');
form.append('file', fileInput.files[0]);
await fetch('/api/upload', { method: 'POST', body: form });
Заголовок Content-Type для FormData вручную не задавайте. Fetch сам поставит multipart/form-data вместе с параметром boundary, разделителем частей. Если прописать тип руками, разделитель потеряется и сервер не разберёт файл.
Заголовки запроса и ответа
Заголовки передают объектом в поле headers: токен авторизации, тип данных, язык. Заголовки ответа читают через response.headers.get():
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1', {
headers: { Accept: 'application/json', Authorization: 'Bearer ВАШ_ТОКЕН' },
});
console.log(response.headers.get('content-type'));
// application/json; charset=utf-8
Какие опции принимает fetch
Полный список опций описан в справочнике RequestInit на MDN. На практике чаще всего нужны эти:
| Опция | Для чего | По умолчанию |
|---|---|---|
method |
HTTP-метод: GET, POST, PUT, PATCH, DELETE | GET |
headers |
заголовки запроса | нет |
body |
тело: строка, FormData, URLSearchParams, Blob | нет |
credentials |
отправлять ли cookie: omit, same-origin, include |
same-origin |
mode |
режим кросс-доменного запроса: cors, no-cors, same-origin |
cors |
signal |
сигнал для отмены и таймаута | нет |
cache |
как использовать HTTP-кеш браузера | default |
redirect |
что делать с редиректом: follow, error, manual |
follow |
Значения по умолчанию легко посмотреть самим: у пустого new Request(url) свойства method, credentials, mode, cache и redirect равны GET same-origin cors default follow.
Как читать ответ сервера
У объекта Response несколько методов для тела. Выбирают их по типу данных, который отдаёт сервер:
| Метод | Что вернёт | Когда нужен |
|---|---|---|
response.json() |
объект или массив | ответ REST API |
response.text() |
строку | HTML, CSV, обычный текст |
response.blob() |
Blob | картинка, PDF, файл для скачивания |
response.formData() |
FormData | ответ в формате формы, встречается редко |
response.arrayBuffer() |
двоичный буфер | аудио, бинарные протоколы |
Кроме тела пригодятся свойства status (код ответа), ok (успех или нет), headers и url, итоговый адрес после редиректов.
Тело читается один раз. Второй вызов упадёт: в Chrome с ошибкой body stream already read, в Node.js с Body is unusable: Body has already been read. Нужен и текст, и JSON? Прочитайте текст и разберите его через JSON.parse() или сделайте копию ответа методом response.clone() до чтения.
Полученный массив дальше обрабатывают обычными средствами языка. Например, новую запись из ответа можно дописать в локальный список, как в инструкции как добавить объект в массив JavaScript, и перерисовать страницу через DOM-дерево.
Обработка ошибок: почему 404 не попадает в catch

Здесь fetch ведёт себя не так, как ждут новички. Руководство MDN по Fetch пишет так: промис отклоняется при некоторых ошибках, например сетевой или при неверной схеме адреса, а если сервер ответил ошибкой вроде 404, fetch() выполняется с объектом Response.
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/999999');
console.log('дошли сюда, status', response.status, 'ok', response.ok);
} catch (error) {
console.log('сюда не попадём');
}
Вывод: дошли сюда, status 404 ok false. Блок catch молчит, и код дальше пытается работать с пустым ответом.
Решение: проверять response.ok самим и бросать ошибку. Мы советуем сразу завести маленькую обёртку и звать её вместо голого fetch:
async function getJSON(url, options = {}) {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
try {
const todo = await getJSON('https://jsonplaceholder.typicode.com/todos/1');
console.log(todo.title);
} catch (error) {
console.error('Запрос не удался:', error.message);
}
Сетевой сбой (сервер выключен, нет интернета, домен не найден) отклонит промис с TypeError. Текст зависит от среды: в Chrome это Failed to fetch, в Node.js fetch failed. Поэтому в условиях удобнее проверять error.name, он одинаковый в обеих средах.
Таймаут и отмена запроса
Своего таймаута у fetch нет: медленный сервер может не отвечать минутами. Ограничение ставят через сигнал отмены.
Таймаут через статический метод AbortSignal
try {
const response = await fetch('/api/slow', { signal: AbortSignal.timeout(5000) });
} catch (error) {
if (error.name === 'TimeoutError') {
console.log('Сервер не ответил за 5 секунд');
}
}
По справке MDN сигнал по истечении времени отменяет запрос с ошибкой TimeoutError. Время считается активное: пока страница лежит в кеше возврата браузера (bfcache), отсчёт фактически стоит на паузе.
Ручная отмена через AbortController
Пригодится, когда пользователь ушёл со страницы или ввёл новый поисковый запрос, а старый ответ уже не нужен:
const controller = new AbortController();
const request = fetch('/api/search?q=js', { signal: controller.signal });
controller.abort(); // например, пользователь продолжил печатать
try {
await request;
} catch (error) {
console.log(error.name); // AbortError
}
Для поиска с подсказками так отменяют предыдущий запрос перед каждым новым. Иначе медленный старый ответ может прийти позже свежего и затереть результаты.
Несколько запросов параллельно
Независимые запросы не ждите по очереди. Promise.all запустит их одновременно и вернёт результаты в том же порядке:
const [user, todos] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/users/1').then(r => r.json()),
fetch('https://jsonplaceholder.typicode.com/todos?userId=1').then(r => r.json()),
]);
Если хотя бы один запрос упадёт, Promise.all отклонится целиком. Когда нужны все ответы, даже с ошибками, берите Promise.allSettled.
CORS и cookie в запросах fetch
Запрос на чужой домен браузер пропустит, только если сервер разрешил это заголовком Access-Control-Allow-Origin. Без разрешения fetch отклоняется с тем же TypeError: Failed to fetch, а причину видно только в консоли:
Access to fetch at 'https://www.iana.org/help/example-domains' from origin
'https://example.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.
Что нужно знать про CORS:
- Чинится на сервере, не во фронтенде. Сервер должен вернуть нужный заголовок, либо запрос идёт через свой бэкенд-прокси.
mode: 'no-cors'ошибку уберёт, но данные не отдаст. Ответ будет «непрозрачным»:typeравенopaque,statusравен 0, тело пустое.- В Node.js CORS не действует, это защита браузера. Поэтому запрос, который «работает в Node и не работает на сайте», почти всегда упирается в CORS.
Cookie на тот же домен fetch отправляет сам, это значение credentials: 'same-origin'. Для запросов на другой домен с сессией пользователя нужен credentials: 'include', и сервер тоже должен это разрешить. Почему без HTTPS такие запросы небезопасны, объясняет статья HTTP и HTTPS: в чём разница.
Частые ошибки при работе с fetch

| Симптом | Причина | Что сделать |
|---|---|---|
В консоли Promise {<pending>} вместо данных |
забыли await перед fetch или перед response.json() |
ждать оба промиса |
Ошибка 404 или 500 не ловится в catch |
на ответ с ошибочным статусом fetch не отклоняется | проверять response.ok и бросать ошибку |
Сервер получил [object Object] |
объект передан в body без сериализации |
JSON.stringify() и заголовок Content-Type: application/json |
Request with GET/HEAD method cannot have body |
тело передано в GET-запросе | данные в строку запроса через URLSearchParams или метод POST |
| Сервер не видит загруженный файл | Content-Type для FormData прописан вручную |
убрать заголовок, fetch поставит его сам |
body stream already read |
тело прочитано второй раз | читать один раз или вызвать response.clone() |
Failed to fetch только в браузере |
CORS или смешанный контент HTTP на HTTPS-странице | смотреть консоль, править заголовки на сервере |
Fetch, XMLHttpRequest или axios
Три способа отправить запрос встречаются в проектах до сих пор. Выбор обычно такой:
| fetch | XMLHttpRequest | axios | |
|---|---|---|---|
| Что это | встроенная функция | встроенный объект, старый способ AJAX | сторонняя библиотека из npm |
| Стиль кода | промисы, async/await |
события и колбэки | промисы, async/await |
| Ошибка на 404 | нет, проверять ok |
нет, проверять status |
да, по умолчанию отклоняет всё вне 200–299 |
| Когда брать | новый код без зависимостей | поддержка старого кода | в проекте уже есть axios или нужны его перехватчики |
Поведение axios на ошибочных статусах описано в документации axios: по умолчанию успешным считается статус от 200 до 299, условие меняют опцией validateStatus. Для учебных проектов и небольших сайтов хватает fetch с обёрткой вроде getJSON выше.
Где освоить JavaScript системно
Fetch закрывает одну задачу: забрать или отправить данные. Чтобы собрать из этого работающий интерфейс, нужны асинхронность, работа с DOM, обработка состояний загрузки и ошибок, а дальше React или Vue. Как выглядит этот путь целиком, мы расписали в статье как стать фронтенд-разработчиком, а чем занят такой специалист каждый день, рассказали в обзоре профессии JavaScript-разработчика.
Ниже курсы по JavaScript из нашего каталога, их можно сравнить по цене, длительности и формату:
| Курс | Школа | Стоимость со скидкой | В рассрочку | Длительность | Обзор курса от Checkroi |
|---|---|---|---|---|---|
| JavaScript Перейти на сайт курса | 65 744 ₽ | 4086 ₽/мес. | 3 месяца | Обзор курса | |
| Fullstack разработчик JavaScript Перейти на сайт курса | 75 000 ₽ | 20 000 ₽/мес. | 6 месяцев | Обзор курса | |
| Факультет fullstack JavaScript Перейти на сайт курса | 134 700 ₽ | 3742 ₽/мес. | 15 месяцев | Обзор курса | |
| Fullstack-разработчик на JavaScript Перейти на сайт курса | 119 070 ₽ | 4961 ₽/мес. | 11 месяцев | Обзор курса | |
| Фулстек-разработчик на JavaScript Перейти на сайт курса | 157 948 ₽ | 4296 ₽/мес. | 11 месяцев | Обзор курса |
Больше программ — в полном каталоге курсов по Javascript




