Learn JavaScript

Ошибки fetch: статус, сеть и неправильный JSON

При работе с fetch важно понимать: “ошибка” бывает разной. Иногда запрос успешно дошел до сервера, но сервер вернул 404. Иногда сеть недоступна. Иногда сервер вернул не JSON, а HTML, и парсер JSON упал.

Надежный код должен различать эти ситуации.

1) Важно: fetch и HTTP-ошибки

fetch не бросает ошибку только из-за статуса 404 или 500. В этих случаях Promise обычно успешно выполняется и возвращает Response, но response.ok будет false.

async function run() {
  const response = await fetch("/api/missing");
  console.log(response.ok);
  console.log(response.status);
}

run();

Значит, статус нужно проверять вручную.

2) Классическая обработка: ok + json + try/catch

async function loadJSON(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }

  const data = await response.json();
  return data;
}

async function run() {
  try {
    const data = await loadJSON("/api/data");
    console.log(data);
  } catch (e) {
    console.log(e.name);
    console.log(e.message);
  }
}

run();

Это базовый шаблон, но он не объясняет, что именно пошло не так. Дальше - типовые виды ошибок.

3) Ошибка статуса (404, 500) - это “ответ сервера”

Ситуация: сервер ответил, но с проблемой. Это не сетевой сбой, это корректный HTTP-ответ.

async function loadJSON(url) {
  const response = await fetch(url);

  if (!response.ok) {
    const message = "Request failed: HTTP " + response.status;
    throw new Error(message);
  }

  return response.json();
}

Часто полезно различать диапазоны:

  • 400-499 - проблема в запросе (неверный URL, нет доступа)
  • 500-599 - проблема на сервере

4) Сетевая ошибка: нет интернета, DNS, соединение

Сетевая ошибка - это когда запроса “как ответа” не получилось. В этом случае fetch отклоняет Promise, и код попадает в catch.

async function run() {
  try {
    await fetch("https://example.invalid");
  } catch (e) {
    console.log(e.name);
    console.log(e.message);
  }
}

run();

Точный текст message зависит от браузера.

5) AbortError: запрос был отменен

Запрос можно отменить через AbortController (например, таймаут или отмена при смене страницы).

async function run() {
  const controller = new AbortController();

  setTimeout(() => controller.abort(), 200);

  try {
    await fetch("/api/data", { signal: controller.signal });
  } catch (e) {
    console.log(e.name); // "AbortError" (обычно)
    console.log(e.message);
  }
}

run();

6) Неправильный JSON: сервер вернул не JSON

Частая причина: сервер вернул HTML-страницу ошибки или текст, а код делает response.json().

async function run() {
  try {
    const response = await fetch("/api/data");

    if (!response.ok) {
      throw new Error("HTTP " + response.status);
    }

    const data = await response.json();
    console.log(data);
  } catch (e) {
    console.log(e.name);
    console.log(e.message);
  }
}

run();

Если JSON невалиден, ошибка будет на этапе response.json().

Полезная проверка: Content-Type

Иногда удобно проверить заголовок:

async function loadJSON(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }

  const type = response.headers.get("content-type") || "";
  if (!type.includes("application/json")) {
    throw new Error("Expected JSON, got: " + type);
  }

  return response.json();
}

7) CORS: запрос запрещен браузером

Если запрос идет на другой домен, браузер может заблокировать его по CORS. Это выглядит как ошибка в fetch, хотя проблема не в коде.

Признак: запрос не дает доступа к данным, и в консоли DevTools есть сообщение про CORS.

Кодом на фронтенде это обычно не лечится - нужен корректный CORS на сервере.

8) Универсальная функция: различать причины

Ниже пример, который дает более понятные сообщения. Он не “магический”, но помогает привести ошибки к одному формату.

async function loadJSON(url, options) {
  let response;

  try {
    response = await fetch(url, options);
  } catch (e) {
    throw new Error("Network error");
  }

  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }

  const type = response.headers.get("content-type") || "";
  if (!type.includes("application/json")) {
    throw new Error("Bad JSON content-type");
  }

  try {
    return await response.json();
  } catch (e) {
    throw new Error("Invalid JSON");
  }
}

async function run() {
  try {
    const data = await loadJSON("/api/data");
    console.log(data);
  } catch (e) {
    console.log(e.message);
  }
}

run();

Что стоит запомнить

  • HTTP 404/500 - это не ошибка fetch, это ответ сервера, проверяется через response.ok и response.status.
  • Сетевые ошибки попадают в catch до Response.
  • Неправильный JSON ломается на response.json().
  • AbortController дает AbortError при отмене.
  • CORS выглядит как ошибка запроса, но решается на стороне сервера.
  • Надежный шаблон: try function fetch(input, init = undefined) { // eslint-disable-line func-name-matching if (!fetchImpl) { // Implement lazy loading of undici module for fetch function const undiciModule = require('internal/deps/undici/undici'); fetchImpl = undiciModule.fetch; } return fetchImpl(input, init); } -> ok/status -> content-type -> json parse -> catch.