TypeError: Failed to fetch — что означает и как исправить

опубликовано
обновлено
читать
≈ 7 мин
автор
среда и версия
Fetch API в современных браузерах
содержание решать задачи Современный JavaScript

Дословно Failed to fetch — «не удалось получить». Браузер сообщает этой фразой ровно одно: доступного ответа не получено. Запрос мог быть заблокирован ещё до отправки. Технически — fetch() не вернул коду доступный объект Response.

Причин почти всегда пять: неверный URL, недоступная сеть, проблема с TLS или mixed content, запрет по CORS или CSP, отменённый запрос. Открой Network и Console: если HTTP-статуса у запроса нет вообще, причина среди этих пяти.

Статусы 404, 401 и 500 сами по себе не отклоняют fetch(). Если ответ доступен коду, промис выполняется с Response, а статус проверяют через response.ok или response.status.

JavaScript / 01

HTTP-ошибка и отклонённый Promise — разные исходы

HTTP-ошибка и отклонённый Promise — разные исходы01 / вход 02 / операция 03 / результат Response доступен status 200–299 response.ok: true Response доступен status 404 / 500 response.ok: false Promise rejected Failed to fetch проверь сеть и CORS01 / вход02 / операция03 / результатResponse доступенstatus 200–299response.ok: trueResponse доступенstatus 404 / 500response.ok: falsePromise rejectedFailed to fetchпроверь сеть и CORS
Статус 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 сообщает о CORSOrigin, 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 может заменить её общим сообщением. Проверь сигнал до повтора запроса. Здесь return await нужен, чтобы отказ промиса попал в локальный catch:

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 намеренно не даёт скрипту полный отчёт о защитных проверках браузера.

Как написать ровно один повтор для идемпотентного запроса и как запускать независимые запросы параллельно через Promise.all, можно отработать в теме «Основы промисов» сборника «Современный JavaScript»: там задачи «Повтор запроса один раз» и «Сборка дашборда из двух запросов» с проверкой на тестах. Тема входит в Premium, первая тема сборника бесплатна.

Источники