Fetch и работа с API из браузера: что знать до начала
Разбираем fetch с нуля: как отправить GET и POST, поймать ошибку сервера, отменить зависший запрос и не упереться в CORS в первый же час работы.

К концу этой статьи вы отправите запрос к публичному API, разберёте JSON-ответ, обработаете ошибку и отмените зависший запрос. На всё уйдёт около 30 минут, если у вас уже есть браузер и текстовый редактор. Ничего устанавливать не нужно: fetch встроен во все актуальные браузеры (Chrome, Firefox, Safari, Edge) и работает без библиотек. Примеры проверены на синтаксисе, актуальном для браузеров 2020 года и новее — то есть везде, кроме давно устаревшего Internet Explorer, где fetch отсутствует.
Что нужно иметь до начала
- Любой современный браузер. Всё, что ниже, можно запускать прямо в консоли разработчика (F12 → вкладка Console).
- Понимание, что такое JSON — текстовый формат вида
{"name": "Иван"}, в котором API обычно отдаёт данные. - Адрес какого-нибудь публичного API для тренировки. В примерах используется
https://jsonplaceholder.typicode.com— бесплатный тестовый сервис, который отдаёт фейковые посты и пользователей. - Базовое знание промисов и
async/await. Если слово «промис» ничего не говорит — прочитайте про них отдельно, здесь мы на них опираемся.
Промис* — объект, который представляет результат операции, ещё не завершившейся к моменту вызова. fetch возвращает промис: код продолжает работать, а когда ответ придёт, промис «разрешится».
Шаг 1. Первый GET-запрос
Откройте консоль браузера и вставьте:
// Получаем один пост по id
async function getPost() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await response.json();
console.log(data);
}
getPost();
В консоли должен появиться объект с полями userId, id, title и body. Если объект есть — запрос прошёл. Обратите внимание на два await: первый ждёт ответа сервера, второй — разбора тела ответа в объект.
Шаг 2. Почему response.json() — отдельный шаг
Начинающие часто думают, что fetch сразу вернёт данные. Нет. Сначала приходит объект Response — это заголовки, статус, но ещё не тело. Тело читается отдельным вызовом, и вариантов несколько:
response.json()— распарсить как JSON;response.text()— получить как строку;response.blob()— для файлов и картинок.
Каждый из них возвращает промис, поэтому и нужен второй await. Тело можно прочитать только один раз: повторный вызов response.json() на том же ответе выбросит ошибку.
Шаг 3. Главная ловушка: fetch не считает 404 ошибкой
Это первое, обо что спотыкаются почти все. Промис fetch отклоняется только при сетевом сбое — нет соединения, не резолвится домен. А вот ответ сервера со статусом 404 или 500 для fetch — это успех: промис разрешается, блок catch не срабатывает.
fetch падает, только когда запрос не дошёл до сервера. Если сервер ответил «страница не найдена» — с точки зрения fetch всё прошло идеально. Статус проверяете вы сами.
Поэтому статус нужно проверять вручную через свойство response.ok (оно true для кодов 200–299):
async function getPostSafe(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
// fetch не бросит ошибку сам — проверяем статус руками
if (!response.ok) {
throw new Error(`Сервер ответил статусом ${response.status}`);
}
return response.json();
}
getPostSafe(99999)
.then(data => console.log(data))
.catch(err => console.error('Не получилось:', err.message));
Запросите несуществующий id (например, 99999) — и увидите в консоли сообщение об ошибке вместо тихого сбоя.
Шаг 4. POST: отправляем данные на сервер
GET по умолчанию, для остального передают второй аргумент — объект настроек. Ключевые поля: метод, заголовки и тело.
async function createPost() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
// тело должно быть строкой, поэтому сериализуем объект
body: JSON.stringify({
title: 'Мой первый пост',
body: 'Текст поста',
userId: 1
})
});
const data = await response.json();
console.log(data);
}
createPost();
В ответе придёт созданный объект с новым id. Две частые ошибки на этом шаге: забыть JSON.stringify и передать объект напрямую (тело уйдёт как [object Object]), либо не поставить заголовок Content-Type — тогда сервер может не понять, что вы прислали JSON.
Основные поля объекта настроек
| Поле | Что задаёт | Пример значения |
|---|---|---|
method | HTTP-метод | 'GET', 'POST', 'PUT', 'DELETE' |
headers | Заголовки запроса | { 'Content-Type': 'application/json' } |
body | Тело запроса (строка) | JSON.stringify(...) |
signal | Сигнал для отмены | из AbortController |
credentials | Слать ли куки | 'include', 'same-origin' |
Шаг 5. Отмена зависшего запроса
Запрос может висеть долго — медленный сервер, ушедший со страницы пользователь. Отменяют запрос через AbortController: создаёте контроллер, передаёте его signal в fetch, а по таймауту или событию вызываете abort().
async function getWithTimeout(url, ms) {
const controller = new AbortController();
// прерываем запрос через ms миллисекунд
const timer = setTimeout(() => controller.abort(), ms);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
clearTimeout(timer); // не забываем убрать таймер
}
}
getWithTimeout('https://jsonplaceholder.typicode.com/posts/1', 3000)
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.warn('Запрос отменён по таймауту');
} else {
console.error(err);
}
});
Когда сработает abort(), промис отклонится ошибкой с name === 'AbortError' — её и ловим отдельно от прочих. В браузерах последних версий есть готовый AbortSignal.timeout(ms), но поддержка более новая, чем у ручного варианта выше, — если пишете для старых окружений, используйте связку с setTimeout.
Если не получилось
- «Failed to fetch» в консоли. Чаще всего это CORS: сервер не разрешил запрос с вашего домена, либо вы обратились по
httpсо страницы наhttps. Проверьте вкладку Network — если запрос красный со статусом (CORS) или ушёл, но браузер заблокировал ответ, проблема на стороне API, а не в вашем коде. - «Unexpected token < in JSON». Сервер вернул HTML (например, страницу ошибки), а вы пытаетесь распарсить его как JSON. Сначала посмотрите
response.statusиawait response.text(), чтобы увидеть, что реально пришло. - «Body has already been read». Вы вызвали
response.json()дважды. Тело читается один раз — сохраните результат в переменную. - Запрос уходит, но данных нет в POST. Забыли
JSON.stringifyдля тела или заголовокContent-Type. - catch не срабатывает на 404. Это не баг: fetch не считает ответ сервера ошибкой. Вернитесь к шагу 3 и добавьте проверку
response.ok.
Пара слов про CORS, чтобы не паниковать
CORS — механизм браузера, который блокирует запросы к чужим доменам, если сервер явно не разрешил их специальными заголовками. Это не ваша ошибка в коде: тот же запрос из терминала через curl пройдёт без проблем. Публичные тренировочные API вроде jsonplaceholder уже настроены на приём запросов откуда угодно, поэтому на них удобно учиться. С реальным сторонним API, который CORS не разрешает, придётся ходить через свой бэкенд-прокси — но это уже отдельная тема.
Что делать дальше
Когда базовый цикл «запрос — проверка статуса — разбор — обработка ошибки» стал привычным, двигайтесь сюда:
- Оберните повторяющийся код в свою функцию-хелпер, которая сама проверяет
response.okи парсит JSON — чтобы не дублировать это в каждом вызове. - Разберитесь с индикатором загрузки: покажите спиннер перед
awaitи уберите вfinally. - Посмотрите на повторные попытки (retry) для временных сбоев сети — но с ограничением числа попыток.
- Изучите вкладку Network в инструментах разработчика: она показывает реальные заголовки, тело и статус каждого запроса и экономит часы отладки.
Инвалидация кеша ответов и стриминг больших тел через response.body — отдельные темы; здесь мы разобрали только базовый цикл работы с API.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: MDN Web Docs — Using the Fetch API, MDN Web Docs — AbortController
Частые вопросы
А если у меня старый браузер и fetch не работает?
Почему мой catch не ловит ошибку 404 или 500?
В чём разница между fetch и библиотекой axios?
Почему запрос падает с «Failed to fetch», хотя код правильный?
Как отправить запрос с авторизацией по токену?
Нужно ли всегда отменять запросы через AbortController?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.