</> Praktum

Главная / JavaScript / Fetch API

Справочник · JavaScript

Fetch API: получаем и отправляем данные

Редакция PraktumОбновлено 7 мин чтенияУровень: новичок

fetch() — встроенная функция браузера для HTTP-запросов. Она принимает адрес и необязательные параметры, а возвращает Promise с объектом Response. Из ответа данные читают методами json(), text() или blob() — они тоже асинхронные. Так страница получает данные с сервера без перезагрузки.

Коротко
  • const res = await fetch(url), затем await res.json().
  • Ответ 404 или 500 — не ошибка для fetch: проверяй res.ok сам.
  • POST с JSON: method, заголовок Content-Type и JSON.stringify.
  • FormData отправляй без заголовка Content-Type — браузер поставит его сам.
  • Таймаут и отмена — через AbortController или AbortSignal.timeout().

Минимальный пример: GET и JSON

async function loadPosts() {
  const response = await fetch('/api/posts');
  if (!response.ok) {
    throw new Error(`Ошибка ${response.status}`);
  }
  return response.json();
}

loadPosts()
  .then((posts) => console.log(posts))
  .catch((error) => console.error(error.message));

Два ожидания: первое — пока придут заголовки ответа, второе — пока тело прочитается и разберётся как JSON. Функция с await внутри должна быть объявлена как async.

Объект Response

Что есть в ответе
Свойство или методЧто даёт
response.oktrue, если статус от 200 до 299
response.statusкод ответа: 200, 404, 500
response.headers.get('content-type')заголовок ответа
await response.json()тело, разобранное как JSON
await response.text()тело строкой
await response.blob()тело как файл: картинка, PDF

Тело можно прочитать только один раз: после json() вызов text() выбросит ошибку. Если нужно и то и другое, прочитай текст и разбери его через JSON.parse.

Параметры запроса

Второй аргумент fetch
ПараметрЗначение
method'GET' по умолчанию, 'POST', 'PUT', 'PATCH', 'DELETE'
headersобъект заголовков: { 'Content-Type': 'application/json' }
bodyтело: строка JSON, FormData, URLSearchParams, Blob
signalсигнал для отмены запроса
credentials'include' — отправлять cookie на чужой домен
cache'no-store' — не брать ответ из кеша

Параметры в адресе

const params = new URLSearchParams({ q: 'flex box', page: 2 });
const response = await fetch(`/api/search?${params}`);
// /api/search?q=flex+box&page=2

URLSearchParams сам экранирует пробелы, кириллицу и символы вроде &. Склеивать адрес вручную через + опасно: запрос «C&A» сломает строку параметров.

POST: отправка JSON

const response = await fetch('/api/comments', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ text: 'Спасибо за урок', lessonId: 12 }),
});
const saved = await response.json();

Объект нельзя передать в body напрямую — его превращают в строку через JSON.stringify. Заголовок Content-Type говорит серверу, что внутри JSON. Сервер должен ожидать именно этот формат.

Отправка формы через FormData

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  const response = await fetch(form.action, {
    method: 'POST',
    body: new FormData(form),   // включая файлы из input type="file"
  });
});

С FormData заголовок Content-Type писать не нужно: браузер сам поставит multipart/form-data с правильной границей между полями. Если указать его вручную, сервер не сможет разобрать тело.

Ошибки: сеть и статус

async function getJSON(url) {
  let response;
  try {
    response = await fetch(url);
  } catch (error) {
    throw new Error('Нет связи с сервером. Проверь интернет и повтори.');
  }
  if (!response.ok) {
    throw new Error(`Сервер ответил ${response.status}`);
  }
  return response.json();
}

Promise от fetch отклоняется только при сетевом сбое, блокировке CORS или отмене. Ответ 404 или 500 — это успешно полученный ответ, и он не попадёт в catch. Поэтому проверка response.ok обязательна в каждом запросе.

Таймаут и отмена запроса

// отменить вручную
const controller = new AbortController();
fetch('/api/search?q=css', { signal: controller.signal });
controller.abort();

// отменить, если ответа нет 5 секунд
const response = await fetch('/api/data', { signal: AbortSignal.timeout(5000) });

Отмена нужна в живом поиске: человек ввёл новый запрос, и старый, медленный ответ не должен приехать позже и затереть новый. При отмене fetch выбрасывает ошибку с именем AbortError (при таймауте — TimeoutError), её обычно не показывают пользователю.

CORS: почему запрос к чужому сайту блокируется

Браузер разрешает читать ответ с другого домена, только если сервер явно это позволил заголовком Access-Control-Allow-Origin. Если заголовка нет, в консоли будет ошибка CORS, а fetch отклонится. Исправить это можно только на сервере или через свой прокси — параметр mode: 'no-cors' не помогает: он даёт пустой непрозрачный ответ, который нельзя прочитать.

Что показать человеку

У каждого запроса четыре состояния, и для каждого нужен свой вид: загрузка (кнопка неактивна, текст «Загружаем…»), успех, пустой результат («Пока ничего нет») и ошибка (что случилось и кнопка «Повторить»). Пустой список и ошибка — разные ситуации, не показывай их одинаково.

fetch или XMLHttpRequest

fetch

Современный API на промисах, работает с async/await, поддерживает отмену и потоки. Используй его.

XMLHttpRequest

Старый API на колбэках. Встречается в старом коде; его единственное преимущество — простое событие прогресса загрузки файла.

Частые ошибки

  • Не проверяют response.ok и пытаются разобрать страницу ошибки 404 как JSON.
  • Забывают await перед response.json() и получают Promise вместо данных.
  • Отправляют объект в body без JSON.stringify.
  • Ставят Content-Type вручную при отправке FormData.
  • Оставляют кнопку активной во время запроса, и человек отправляет форму несколько раз.

Доступность

Пока идёт запрос, сообщай об этом не только спиннером: добавь текст «Загружаем…» в область с aria-live="polite", а у кнопки — атрибут disabled или aria-busy="true" у контейнера. Ошибку выводи текстом рядом с действием и давай способ повторить.

Поддержка браузерами

fetch работает во всех актуальных браузерах и в Node.js начиная с 18-й версии. AbortController поддерживается везде, AbortSignal.timeout() — в браузерах с 2022 года. Internet Explorer fetch не поддерживает.

Практика

Загрузи список постов с https://jsonplaceholder.typicode.com/posts?_limit=5 и выведи заголовки списком. Пока данные едут, показывай «Загружаем…», при ошибке — сообщение и кнопку «Повторить». Если ответа нет 5 секунд, отменяй запрос.

Показать решение
const status = document.querySelector('.status');
const list = document.querySelector('.posts');

async function load() {
  status.textContent = 'Загружаем…';
  try {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5', {
      signal: AbortSignal.timeout(5000),
    });
    if (!res.ok) throw new Error(`Ошибка ${res.status}`);
    const posts = await res.json();
    list.innerHTML = '';
    for (const post of posts) {
      const li = document.createElement('li');
      li.textContent = post.title;
      list.append(li);
    }
    status.textContent = '';
  } catch (error) {
    status.textContent = 'Не удалось загрузить посты.';
  }
}

load();
document.querySelector('.retry').addEventListener('click', load);

Открыть песочницу

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

Почему fetch не попадает в catch при ошибке 404?

Для fetch ответ 404 — успешно полученный ответ сервера. catch ловит только сетевые сбои, CORS и отмену. Проверяй response.ok и выбрасывай ошибку сам.

Как отправить POST-запрос с JSON?

Передай method: 'POST', заголовок 'Content-Type': 'application/json' и body: JSON.stringify(данные).

Как сделать таймаут для fetch?

Передай signal: AbortSignal.timeout(5000) — запрос отменится через 5 секунд. В старых браузерах — AbortController и setTimeout с вызовом abort().

Что значит ошибка CORS?

Сервер другого домена не разрешил читать свой ответ. Решается настройкой заголовка Access-Control-Allow-Origin на сервере или запросом через свой сервер.

Почему response.json() возвращает Promise?

Тело ответа может приходить частями, и браузер читает его асинхронно. Поэтому перед response.json() тоже нужен await.

Источники

Практикумы по теме

Ещё в разделе JavaScript