Fetch обёртка с обработкой ошибок

Тонкая обёртка над нативным fetch, которая бросает исключение при HTTP-статусах 4xx/5xx и разделяет сетевые и HTTP-ошибки.

Задача

fetch не бросает ошибку при 404 или 500response.ok просто равен false. Копировать эту проверку в каждый вызов неудобно; нужна единая утилита для всего приложения.

Решение

// utils/fetch.js

class HttpError extends Error {
  constructor(response) {
    super(`HTTP ${response.status}: ${response.statusText}`);
    this.name = 'HttpError';
    this.status = response.status;
    this.response = response;
  }
}

async function fetchJSON(url, options = {}) {
  let response;

  try {
    response = await fetch(url, {
      headers: { 'Content-Type': 'application/json', ...options.headers },
      ...options,
    });
  } catch (err) {
    // Сетевая ошибка: нет интернета, CORS, DNS
    throw new Error(`Сетевая ошибка: ${err.message}`);
  }

  if (!response.ok) {
    throw new HttpError(response);
  }

  // 204 No Content — тела нет
  if (response.status === 204) return null;

  return response.json();
}

export { fetchJSON, HttpError };

Использование:

import { fetchJSON, HttpError } from './utils/fetch.js';

// GET
const users = await fetchJSON('/api/users');

// POST
const created = await fetchJSON('/api/users', {
  method: 'POST',
  body: JSON.stringify({ name: 'Alice' }),
});

// Обработка ошибок
try {
  await fetchJSON('/api/protected');
} catch (err) {
  if (err instanceof HttpError) {
    if (err.status === 401) redirectToLogin;
    if (err.status === 404) showNotFound;
  } else {
    showNetworkError(err.message);
  }
}

Ключевые моменты

  • fetch не бросает ошибку при 4xx/5xx — всегда проверяй response.ok.
  • Разделяй сетевые ошибки (catch блок) и HTTP-ошибки (response.ok === false).
  • HttpError хранит status и response — можно дочитать тело с сообщением от API.
  • 204 No Content требует отдельной проверки перед вызовом response.json.

Варианты

  • axios — делает то же из коробки плюс interceptors, timeout, отмена запросов.
  • ky — fetch-обёртка с retry, hooks, timeout; ~3 KB.
  • Для TypeScript — см. Рецепт -- API-клиент с TypeScript.

Связанные рецепты / темы