Дословно Failed to fetch — «не удалось получить». Браузер сообщает этой фразой ровно одно: доступного ответа не получено. Запрос мог быть заблокирован ещё до отправки. Технически — fetch() не вернул коду доступный объект Response.
Причин почти всегда пять: неверный URL, недоступная сеть, проблема с TLS или mixed content, запрет по CORS или CSP, отменённый запрос. Откройте Network и Console: если HTTP-статуса у запроса нет вообще, причина среди этих пяти.
Статусы 404, 401 и 500 сами по себе не отклоняют fetch(). Если ответ доступен коду, промис выполняется с Response, а статус проверяют через response.ok или response.status.
HTTP-ошибка и отклонённый Promise — разные исходы
404 приходит по ветке Response. Failed to fetch относится к ветке без доступного ответа, а точную причину обычно показывает не JavaScript, а DevTools.Почему fetch не считает 404 и 500 сетевой ошибкой
Проверяйте response.ok до чтения тела. Это свойство равно true для статусов от 200 до 299:
async function getJson(url, options) {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
Теперь у приложения остаются разные сигналы:
Error: HTTP 404означает, что сервер ответил и код сам отклонил такой статус;TypeError: Failed to fetchозначает, чтоfetch()не вернул доступныйResponse;SyntaxErrorприresponse.json()означает, что ответ пришёл, но его тело не оказалось допустимым JSON.
Первый случай — обычный HTTP-ответ: 401 требует проверить учётные данные, 404 — адрес и наличие ресурса, 500 означает серверную ошибку. Сам статус 500 не делает повтор безопасным: сервер мог уже выполнить операцию. Повторяйте запрос только при идемпотентной операции, серверной защите от дублирования или подтверждении, что первый запрос не был применён. Что значит каждый класс и какой возвращать самому, разобрано в справочнике кодов ответов HTTP.
Не заменяйте все три случая одной строкой throw new Error('Failed to fetch'): она стирает исходную ошибку и делает диагностику сложнее.
Как исправить Failed to fetch: диагностика по шагам
Откройте DevTools до повторения ошибки. На вкладке Network включите сохранение журнала при переходах, очистите список и снова выполните действие. Затем проверьте наблюдения по порядку.
1. Какой URL получил браузер
Откройте запись запроса и сверьте Request URL, схему, домен, порт и путь. Относительный адрес вычисляется от базового URL документа, поэтому /api/users и api/users на вложенной странице могут привести к разным путям.
Перед fetch() адрес можно проверить штатным классом URL:
const requestUrl = new URL(endpoint, document.baseURI)
console.log(requestUrl.href)
const response = await fetch(requestUrl)
Если new URL() выбросил TypeError, исправьте входное значение. Если адрес корректен, но ведёт не на тот хост или порт, исправьте базовый URL или конфигурацию окружения. Не добавляйте протокол, порт и слеши строковыми склейками.
Если строки запроса в Network нет, убедитесь, что выполнение дошло до fetch(): поставьте breakpoint на эту строку или временно выведите вычисленный URL. Синтаксическая ошибка await is only valid in async functions останавливает код раньше, поэтому к Failed to fetch не относится.
2. Есть ли HTTP-статус
Числовой статус 4xx или 5xx подтверждает ответ сервера, но ещё не гарантирует доступ JavaScript к нему: отдельно проверьте Console на CORS. Если CORS-сообщения нет и код получил Response, возвращайтесь к проверке response.ok.
Статусы (failed), CORS error и (blocked:…) в Network указывают на ветку без обычного ответа для кода. Откройте запись и посмотрите Headers, Initiator и Timing, затем прочитайте соседнее сообщение в Console. Текст статуса и набор вкладок зависят от браузера, поэтому не стройте логику приложения на строках ошибок DevTools.
3. На каком участке произошёл сбой
| Наблюдение | Что проверить | Исправление |
|---|---|---|
| Ошибка разрешения имени, соединения или ожидания | Домен, VPN или прокси, доступность API, порт | Исправить адрес или сетевой маршрут; проверить, что сервис запущен |
| Ошибка сертификата или TLS | Срок и цепочку сертификата, имя хоста | Исправить сертификат на сервере; не отключать проверку TLS у пользователей |
| HTTPS-страница вызывает HTTP API | В Console есть сообщение о mixed content | Перевести API и URL запроса на HTTPS |
Console сообщает о connect-src | Заголовок Content-Security-Policy документа | Добавить нужный origin в серверную политику connect-src |
| Console сообщает о CORS | Origin, preflight OPTIONS и ответные CORS-заголовки | Исправить разрешения на сервере по точной причине из Console |
| Запрос отменён | Переданный AbortSignal, таймер и вызов abort() | Считать ожидаемую отмену отдельным исходом, а не сетевым сбоем |
| Ошибка исчезает в чистом профиле | Блокировщик рекламы, privacy-расширение или защитное ПО | Найти правило или домен, который блокируется; не просить всех отключать защиту |
Адрес API можно открыть в отдельной вкладке только как дополнительную проверку DNS, TLS и доступности обычного GET. Такая проверка не повторяет метод, заголовки, тело, CSP и CORS исходного fetch(), поэтому успешная страница не доказывает, что запрос из приложения разрешён.
Сеть, DNS, TLS и офлайн-режим
При сетевом отказе браузер может показать в Network более точную фазу: DNS lookup, соединение, согласование TLS или ожидание ответа. Сам объект ошибки в JavaScript не обязан раскрывать эту фазу. Записывайте для поддержки URL без секретных параметров, время сбоя и наблюдение из DevTools, но не пытайтесь извлечь причину разбором текста TypeError.
Проверьте, не выбран ли в Network режим Offline, и открывается ли заведомо доступный ресурс того же приложения. navigator.onLine годится только как подсказка:
if (!navigator.onLine) {
showNotice('Похоже, устройство не подключено к сети')
}
Значение true не подтверждает доступ к интернету или конкретному API: устройство может видеть локальную сеть, но не сервер. Поэтому не отменяйте запросы только на основании navigator.onLine.
Mixed content, CSP и CORS
Если страница открыта по HTTPS, запрос fetch() к обычному удалённому HTTP-origin относится к блокируемому смешанному контенту. Loopback-адреса вроде localhost и 127.0.0.1 браузер может считать потенциально доверенными, поэтому проверяйте фактическое сообщение в Console. Для удалённого API исправление находится в адресе и настройке сервера: он должен быть доступен по HTTPS. Отключение защиты браузера скрывает дефект только на машине разработчика.
CSP ограничивает подключения директивой connect-src. При блокировке Console указывает нарушенную директиву и адрес. Разрешайте только нужный origin в политике, которую сервер отдаёт для документа; try…catch не может обойти CSP.
CORS относится к чтению ответа от другого origin. По соображениям безопасности JavaScript получает общий сбой, а подробности остаются в Console. Не лечите его опцией mode: 'no-cors': она возвращает непрозрачный ответ со статусом 0, без доступных заголовков и тела. Разбор preflight и серверных заголовков есть в статье blocked by CORS policy.
Отмена запроса и расширения
Запрос с AbortSignal может завершиться потому, что код сам вызвал abort(), сработал таймер или компонент уничтожился. Нативный AbortController обычно отклоняет промис с DOMException по имени AbortError, но обёртка над Fetch может заменить её общим сообщением. Проверьте сигнал до повтора запроса:
try {
return await getJson('/api/profile', { signal })
} catch (error) {
if (signal.aborted) {
console.info('Запрос отменён:', signal.reason)
return null
}
throw error
}
Если приложение не отменяло запрос, воспроизведите проблему в чистом профиле браузера. Исчезновение ошибки указывает на локальное расширение, фильтр DNS, антивирус или прокси, но не показывает виновника автоматически. Возвращайте расширения по одному и сверяйте домен или правило блокировки.
Что сообщать пользователю
Интерфейс не должен обещать точную причину, которой у кода нет. Для неожиданного отказа достаточно сообщения «Не удалось связаться с сервером»; кнопку повтора добавляйте, только если операция допускает безопасный повтор. Ожидаемую отмену показывать как ошибку не нужно, а HTTP-ответы следует обрабатывать по их статусу.
В журнал разработчика передавайте исходный объект ошибки через cause или систему сбора ошибок, не включая токены и персональные данные из URL. Подробности CORS, CSP, DNS и TLS всё равно придётся сверять по DevTools или серверным журналам: Fetch намеренно не даёт скрипту полный отчёт о защитных проверках браузера.