Fetch в JavaScript: запросы к серверу без боли

Содержание

О проверке кода. Примеры не запускались в браузере — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».

Общение с сервером — сердце современного веба. fetch заменил громоздкий XMLHttpRequest и внешние библиотеки простым API на промисах. Разберём его правильное применение.

Простой GET-запрос

async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);   // 1. ждём ответ
  const user = await response.json();                 // 2. ждём разбор тела
  return user;
}

Два await — не случайность. Первый ждёт, пока сервер вернёт ответ (заголовки, статус). Второй — response.json() — ждёт загрузки и разбора тела: оно приходит потоком отдельно от заголовков, поэтому его получение тоже асинхронно. Кроме .json() есть .text(), .blob() (файлы), .formData().

Главная ловушка: fetch не бросает на 404

// ❌ наивно — обработает ошибку сервера как успех
const response = await fetch('/api/users/999');
const user = await response.json();   // 404 придёт сюда как «успех»!

// ✅ проверяем статус вручную
const response = await fetch('/api/users/999');
if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);   // 404, 500 — сами бросаем
}
const user = await response.json();

fetch отклоняет промис только при сетевом сбое — нет интернета, оборвано соединение, DNS не разрешился. HTTP-ошибки 404 и 500 для него — успешный ответ сервера, ведь ответ пришёл. Поэтому статус проверяют вручную через response.ok (true для 200–299). Это отличие от библиотек вроде axios, которые бросают на ошибочных статусах, и частый источник багов.

Правильный порядок работы с fetch
  1. 1 await fetch(url)Ждём ответ сервера
  2. 2 Проверить response.ok404/500 fetch НЕ считает ошибкой — проверяем сами
  3. 3 await response.json()Разбираем тело ответа
  4. 4 try/catch вокруг всегоЛовим сетевые сбои и брошенные ошибки

POST с JSON

async function createUser(data) {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),          // объект → строка JSON
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

createUser({ name: 'Аня', age: 30 });

Отправка данных требует трёх вещей: method: 'POST', заголовок Content-Type: application/json и тело body с JSON.stringify(data) — объект нужно вручную превратить в строку. Забыть заголовок или stringify — частая ошибка, сервер не поймёт данные. Аналогично шлют PUT, PATCH, DELETE.

Полная обработка ошибок

async function loadData(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Сервер вернул ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    // сюда попадут И сетевые сбои, И брошенный выше HTTP-статус
    console.error('Ошибка загрузки:', error.message);
    return null;
  }
}

try/catch вокруг всего ловит два вида проблем: сетевые сбои (их бросает сам fetch) и HTTP-ошибки (которые бросаете вы после проверки response.ok). Это обычная обработка ошибок async/await — асинхронный код ловят синхронно выглядящим try/catch.

Отмена запроса

const controller = new AbortController();

fetch('/api/search?q=node', { signal: controller.signal })
  .then(r => r.json())
  .catch(e => {
    if (e.name === 'AbortError') console.log('запрос отменён');
  });

// отменить — например, пользователь ввёл новый запрос
controller.abort();

AbortController отменяет запрос на лету. Передаёте signal в fetch, а вызов controller.abort() прерывает запрос. Классический случай — живой поиск: пользователь печатает, каждый новый символ шлёт запрос, а предыдущий (уже неактуальный) отменяют. Это экономит трафик и предотвращает гонку ответов. Часто сочетается с троттлингом ввода.

Параллельные запросы

// ❌ последовательно — медленно (сумма времён)
const user = await (await fetch('/api/user')).json();
const posts = await (await fetch('/api/posts')).json();

// ✅ параллельно — быстро (время самого долгого)
const [user, posts] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
]);

Если запросы не зависят друг от друга, не ждите их по очереди. Promise.all запускает все сразу, и общее время равно самому долгому запросу, а не их сумме. Деструктуризация массива раскладывает результаты по переменным. Это одна из главных оптимизаций в работе с сетью.


Смежные темы: промисы и async/await — основа fetch; JSON — формат данных; обработка ошибок; debounce и throttle — для живого поиска. Полный список — в уроках JavaScript.

Частые вопросы

Что такое fetch?
Встроенная функция для HTTP-запросов к серверу из браузера и Node.js. Возвращает промис с ответом. Заменила старый громоздкий XMLHttpRequest и внешнюю библиотеку для AJAX-запросов простым современным API.
Почему fetch не выдаёт ошибку на 404?
fetch отклоняет промис только при сетевом сбое — нет соединения, оборван запрос. HTTP-статусы 404 и 500 считаются успешным ответом сервера, поэтому статус проверяют вручную через response.ok перед разбором тела.
Как отправить JSON через fetch?
В POST-запросе указывают method POST, заголовок Content-Type application/json и тело body с JSON.stringify(данные). Сервер получит объект в формате JSON. Для чтения ответа вызывают response.json().
Почему нужно два await у fetch?
Первый await ждёт ответ сервера (заголовки), второй — response.json() ждёт загрузки и разбора тела ответа. Тело приходит потоком отдельно от заголовков, поэтому его получение — тоже асинхронная операция.
Как отменить fetch-запрос?
Через AbortController: создают контроллер, передают его signal в fetch, а для отмены вызывают controller.abort(). Полезно, чтобы прервать устаревший запрос, например при быстрой смене поискового ввода.