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

Вы написали fetch('/api/users'), открыли консоль — а там либо пустой объект вместо данных, либо красная строка про CORS, либо запрос вроде прошёл, но .json() падает. К концу этого материала вы будете понимать, почему fetch не бросает ошибку на статусе 404, зачем нужен await response.json() отдельным шагом и что означает CORS-ошибка, которую нельзя починить на фронтенде. Займёт минут двадцать чтения и один открытый проект под рукой.
Всё ниже — про нативный fetch, встроенный во все современные браузеры (Chrome, Firefox, Safari актуальных версий) и в Node.js начиная с версии 18. Отдельных библиотек не требуется.
Что нужно знать до начала
- Браузер с открытой панелью разработчика (F12), вкладки Console и Network. Больше половины проблем с fetch видно именно там.
- Понимание, что fetch возвращает Promise — то есть работает асинхронно. Если это слово ничего не говорит, разберите промисы отдельно, иначе дальше будет тяжело.
- Работающий эндпоинт, который отдаёт JSON. Для примеров подойдёт публичный
https://jsonplaceholder.typicode.com/users.
Ошибка 1. Забыли, что fetch асинхронный
Самая частая ловушка. Вы пишете так и удивляетесь, что data — не массив, а что-то странное:
// НЕПРАВИЛЬНО
const data = fetch('https://jsonplaceholder.typicode.com/users');
console.log(data); // Promise, а не данныеfetch не возвращает данные — он возвращает обещание, что данные когда-нибудь будут. Нужно дождаться результата. Два рабочих способа.
Через async/await — читается как обычный код:
async function loadUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
console.log(users); // массив объектов
}
loadUsers();Через цепочку .then() — то же самое иначе:
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((users) => console.log(users));Как понять, что получилось: в консоли вы видите массив объектов, а не строку Promise {<pending>}.
Ошибка 2. Ждут, что fetch бросит ошибку на 404 и 500
Это отличает fetch от привычной по другим языкам логики. Запрос, который вернул статус 404 или 500, для fetch — успех. Промис отклоняется (reject) только при сетевом сбое: нет соединения, оборвался DNS, сработал CORS. Ответ сервера с любым статусом — это выполненный промис.
fetch считает своей работой доставить ответ, а не оценивать его. Статус 500 — это тоже доставленный ответ. Проверять код обязаны вы.
Значит, полагаться на try/catch для отлова 404 нельзя — блок catch туда не зайдёт. Проверяйте response.ok вручную:
async function loadUsers() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
// response.ok === true только для статусов 200–299
if (!response.ok) {
throw new Error(`Сервер ответил ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
// сюда попадёт и сетевой сбой, и наш throw выше
console.error('Не удалось загрузить пользователей:', error.message);
}
}Ошибка 3. Вызывают response.json() вслепую
Тело ответа читается один раз и превращается в поток. Две проблемы новичков.
Первая: .json() тоже возвращает промис, его тоже нужно ждать. Забыли await — получили промис вместо данных.
Вторая: если сервер вернул не JSON (например, HTML со страницей ошибки или пустое тело), .json() упадёт с SyntaxError: Unexpected token < in JSON. Классика: эндпоинт отдал HTML-страницу 500-й ошибки, а вы пытаетесь распарсить её как JSON.
И третье, о чём почти все спотыкаются хоть раз: тело можно прочитать только один раз. Нельзя вызвать response.json(), а потом response.text() для того же ответа — второй вызов бросит ошибку про уже использованный поток. Если нужно и то и другое, сначала возьмите text(), потом сами разберите через JSON.parse().
Ошибка 4. Путают CORS-ошибку с ошибкой в своём коде
Вы видите в консоли что-то вроде Access to fetch at '...' from origin '...' has been blocked by CORS policy и начинаете переписывать fetch. Не поможет. CORS — это правило браузера, а разрешение выдаёт сервер, к которому вы обращаетесь, через заголовки ответа.
Что важно понять новичку:
- CORS-ошибку нельзя исправить на фронтенде. Никакая опция в fetch её не снимет.
- Заголовок
Access-Control-Allow-Originобязан прислать сервер. Если это чужой публичный API без поддержки CORS — из браузера вы к нему не достучитесь. - Опция
mode: 'no-cors'не решает проблему, а маскирует: вы получите «непрозрачный» ответ, тело которого прочитать нельзя. Для реальных данных она бесполезна.
Решения — на стороне сервера или инфраструктуры: настроить CORS-заголовки на своём бэкенде либо ходить к чужому API через собственный прокси (свой сервер запрашивает данные и отдаёт их вашему фронтенду, браузерных ограничений между двумя серверами нет).
Ошибка 5. Отправляют POST и забывают заголовок и сериализацию
GET работает «из коробки», а на первом POST многие спотыкаются. Тело нужно сериализовать в строку вручную, а сервер должен знать, что это JSON:
async function createUser(name, email) {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
// объект нужно превратить в строку сами
body: JSON.stringify({ name, email }),
});
if (!response.ok) {
throw new Error(`Сервер ответил ${response.status}`);
}
return response.json();
}Без JSON.stringify в тело уйдёт строка [object Object], а сервер вернёт ошибку валидации. Без Content-Type: application/json бэкенд может не распознать тело и решить, что вы прислали пустой запрос.
Отдельно про cookie
По умолчанию fetch не отправляет cookie на другой домен. Если авторизация держится на cookie-сессии и запрос идёт на другой origin, добавьте credentials: 'include' — иначе сервер увидит вас как неавторизованного. Для этого сервер, в свою очередь, должен разрешить credentials в CORS-заголовках.
Что видно в панели Network
Прежде чем гадать, откройте вкладку Network и найдите свой запрос. Там сразу понятно, где сломалось.
| Что вы видите | Где проблема |
|---|---|
| Статус 404 / 500 в списке запросов | Не в fetch, а в URL или на сервере. Проверьте адрес и метод |
| Запроса вообще нет в списке | Код до fetch не дошёл — ошибка выше по логике |
| Красная строка про CORS в консоли | Заголовки сервера, а не ваш код |
| Статус 200, но тело пустое или HTML | Сервер отдал не то, что вы парсите через .json() |
| Запрос висит в статусе pending | Сервер не отвечает или нет таймаута — см. ниже |
Про зависшие запросы и таймаут
У fetch нет встроенного таймаута. Если сервер молчит, промис будет ждать сколько угодно, а пользователь смотреть на спиннер. Отменять запрос нужно через AbortController:
async function loadWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
clearTimeout(timer);
}
}Если сработал таймаут, fetch отклонится с ошибкой, у которой error.name === 'AbortError' — по ней можно отличить отмену от обычного сбоя.
Что делать дальше
Соберите привычку сразу оборачивать fetch в функцию с проверкой response.ok и обработкой catch — тогда пять из шести ошибок отсюда просто не возникнут. Дальше стоит посмотреть в сторону обработки повторов при сетевых сбоях и отмены устаревших запросов при быстрой смене экранов. Полная спецификация возможностей fetch — в разделе Fetch API на MDN; там же описаны все опции второго аргумента, включая заголовки, режимы и работу с потоками ответа.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Fetch API — MDN Web Docs, AbortController — MDN Web Docs
Частые вопросы
Почему try/catch не ловит ошибку 404?
Как убрать CORS-ошибку в fetch?
У меня старый браузер или Node.js, fetch не работает — что делать?
Почему .json() падает с ошибкой Unexpected token?
Запрос висит и не завершается — как поставить таймаут?
Почему на другом домене не отправляются cookie авторизации?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.