Learn JavaScript

fetch() - сетевой запрос в JavaScript

fetch() отправляет сетевой запрос и возвращает промис с ответом от сервера.

Чаще всего fetch() используют, когда нужно:

  • загрузить данные из API
  • отправить форму или JSON на сервер
  • получить текст, HTML, файл или изображение
  • проверить статус ответа
  • работать с запросами через async/await

Главная мысль: fetch() не возвращает сразу готовые данные. Сначала он возвращает объект Response, а уже из него нужно отдельно прочитать тело ответа.

Синтаксис

Базовая запись выглядит так:

fetch(url, options)

Где:

  • url - адрес запроса
  • options - необязательный объект с настройками запроса

Самый простой запрос можно написать так:

fetch("/api/users")

Если нужны метод, заголовки или тело запроса, передают второй аргумент:

fetch("/api/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Анна" }),
});

Что возвращает fetch()

fetch() всегда возвращает Promise.

Если запрос удалось выполнить на сетевом уровне, этот промис успешно завершится объектом Response.

const promise = fetch("/api/users");

console.log(promise instanceof Promise); // true

Чтобы получить сам ответ, нужно использовать then() или await.

fetch("/api/users").then((response) => {
  console.log(response); // Response
});

Объект Response содержит статус, заголовки и методы для чтения тела ответа.

Простой пример с JSON

Чаще всего сервер возвращает JSON.

fetch("/api/users")
  .then((response) => response.json())
  .then((users) => {
    console.log(users);
  });

Здесь происходит два асинхронных шага:

  • fetch() ждёт ответ сервера
  • response.json() читает тело ответа и превращает JSON в значение JavaScript

response.json() тоже возвращает промис, поэтому его нужно дождаться.

Пример с async/await

С async/await код обычно читается проще.

async function loadUsers() {
  const response = await fetch("/api/users");
  const users = await response.json();

  console.log(users);
}

loadUsers();

Такой вариант удобен, когда нужно последовательно выполнить запрос, проверить ответ и затем обработать данные.

fetch() не читает тело ответа автоматически

Это один из главных нюансов.

После await fetch() у вас есть только объект Response, а не готовые данные.

async function loadUser() {
  const response = await fetch("/api/user");

  console.log(response); // Response
}

Чтобы получить данные, нужно вызвать подходящий метод:

async function loadUser() {
  const response = await fetch("/api/user");
  const user = await response.json();

  console.log(user.name);
}

Для JSON используют response.json(), для обычного текста - response.text().

Как читать разные типы ответа

У объекта Response есть несколько методов для чтения тела.

const response = await fetch("/api/data");

const data = await response.json();

Основные варианты:

  • response.json() - прочитать JSON и превратить его в значение JavaScript
  • response.text() - прочитать обычный текст
  • response.blob() - прочитать файл или изображение как Blob
  • response.formData() - прочитать данные формы
  • response.arrayBuffer() - прочитать бинарные данные

Пример с текстом:

const response = await fetch("/about.html");
const html = await response.text();

console.log(html);

Важно: тело ответа можно прочитать только один раз.

const response = await fetch("/api/user");

const user = await response.json();

const text = await response.text(); // ошибка: тело уже прочитано

Если нужно прочитать ответ двумя разными способами, обычно проблема в логике обработки. На практике заранее выбирают один формат.

Статус ответа

Объект Response позволяет узнать HTTP-статус.

const response = await fetch("/api/users");

console.log(response.status); // например, 200
console.log(response.statusText); // например, "OK"
console.log(response.ok); // true или false

Свойство response.ok равно true, если статус находится в диапазоне от 200 до 299.

async function loadUsers() {
  const response = await fetch("/api/users");

  if (response.ok) {
    const users = await response.json();
    console.log(users);
  } else {
    console.log("Сервер вернул ошибку:", response.status);
  }
}

Это самая частая проверка после запроса.

HTTP-ошибки не попадают в catch автоматически

Это очень важный момент.

Если сервер ответил 404 или 500, fetch() обычно не считает это ошибкой промиса. Запрос был выполнен, сервер ответил, поэтому промис успешно завершится объектом Response.

try {
  const response = await fetch("/api/unknown-page");

  console.log(response.status); // 404
  console.log(response.ok); // false
} catch (error) {
  console.log("Сюда код обычно не попадёт из-за 404");
}

catch сработает, например, при сетевой ошибке, проблеме CORS или отмене запроса.

Поэтому HTTP-статус лучше проверять вручную:

async function loadUsers() {
  const response = await fetch("/api/users");

  if (!response.ok) {
    throw new Error(`Ошибка HTTP: ${response.status}`);
  }

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

Теперь неуспешный HTTP-статус можно обработать через catch.

loadUsers()
  .then((users) => {
    console.log(users);
  })
  .catch((error) => {
    console.log(error.message);
  });

Обработка ошибок через try…catch

С async/await ошибки обычно обрабатывают через try...catch.

async function loadUsers() {
  try {
    const response = await fetch("/api/users");

    if (!response.ok) {
      throw new Error(`Ошибка HTTP: ${response.status}`);
    }

    const users = await response.json();
    console.log(users);
  } catch (error) {
    console.log("Не удалось загрузить пользователей");
    console.log(error.message);
  }
}

В такой обработчик попадут:

  • сетевые ошибки
  • отмена запроса
  • ошибка, которую вы выбросили вручную при !response.ok
  • ошибка чтения JSON, если тело ответа оказалось невалидным JSON

GET-запрос с параметрами

Для параметров удобно использовать URL и searchParams.

const url = new URL("/api/products", window.location.origin);

url.searchParams.set("category", "books");
url.searchParams.set("limit", "10");

const response = await fetch(url);
const products = await response.json();

console.log(products);

В итоге запрос уйдёт примерно на такой адрес:

/api/products?category=books&limit=10

Такой способ лучше ручной склейки строк, потому что URLSearchParams корректно кодирует специальные символы.

const url = new URL("/api/search", window.location.origin);

url.searchParams.set("q", "javascript уроки");

console.log(url.toString());

Пробелы и другие символы будут подготовлены для URL автоматически.

POST-запрос с JSON

Чтобы отправить данные на сервер, обычно указывают method, headers и body.

async function createUser() {
  const response = await fetch("/api/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: "Анна",
      age: 25,
    }),
  });

  if (!response.ok) {
    throw new Error(`Ошибка HTTP: ${response.status}`);
  }

  const createdUser = await response.json();
  console.log(createdUser);
}

Объект JavaScript нельзя просто положить в body. Его нужно превратить в строку, например через JSON.stringify().

body: JSON.stringify({ name: "Анна" })

Заголовок Content-Type помогает серверу понять, что тело запроса содержит JSON.

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

Если данные пришли из формы, можно использовать FormData.

const form = document.querySelector("form");

form.addEventListener("submit", async (event) => {
  event.preventDefault();

  const formData = new FormData(form);

  const response = await fetch("/api/profile", {
    method: "POST",
    body: formData,
  });

  if (!response.ok) {
    console.log("Ошибка отправки формы");
    return;
  }

  console.log("Форма отправлена");
});

Для FormData обычно не нужно вручную ставить Content-Type. Браузер сам добавит правильный заголовок с нужной границей для multipart-данных.

Заголовки ответа

Заголовки ответа доступны через response.headers.

const response = await fetch("/api/users");

console.log(response.headers.get("content-type"));

Например, можно проверить, что сервер действительно вернул JSON.

const response = await fetch("/api/users");
const contentType = response.headers.get("content-type");

if (contentType && contentType.includes("application/json")) {
  const users = await response.json();
  console.log(users);
} else {
  console.log("Сервер вернул не JSON");
}

Не все заголовки доступны JavaScript-коду в браузере. Некоторые ограничены правилами безопасности и CORS.

Отправка заголовков запроса

Заголовки запроса передают в options.headers.

const response = await fetch("/api/profile", {
  headers: {
    Authorization: "Bearer token",
    Accept: "application/json",
  },
});

Это часто используется для токенов, форматов ответа и других служебных данных.

Но не все заголовки можно выставлять вручную. Часть заголовков браузер контролирует сам.

credentials и cookies

По умолчанию fetch() в браузере отправляет cookies для запросов на тот же сайт.

Если нужно явно указать поведение, используют настройку credentials.

fetch("/api/profile", {
  credentials: "same-origin",
});

Основные варианты:

  • "same-origin" - отправлять cookies только на тот же origin
  • "include" - отправлять cookies даже для кросс-доменных запросов, если сервер это разрешает
  • "omit" - не отправлять cookies

Для запросов на другой домен одного credentials: "include" недостаточно. Сервер тоже должен разрешить такой запрос через CORS.

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

fetch() можно отменить с помощью AbortController.

const controller = new AbortController();

fetch("/api/users", {
  signal: controller.signal,
})
  .then((response) => response.json())
  .then((users) => {
    console.log(users);
  })
  .catch((error) => {
    console.log(error.name); // "AbortError"
  });

controller.abort();

На практике это полезно, когда пользователь ушёл со страницы, изменил поисковый запрос или запрос выполняется слишком долго.

Пример с простым таймаутом:

async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();

  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeout);

  try {
    const response = await fetch(url, {
      signal: controller.signal,
    });

    return response;
  } finally {
    clearTimeout(timeoutId);
  }
}

У fetch() нет обычной настройки timeout, поэтому отмену часто делают именно через AbortController.

Несколько запросов параллельно

Так как fetch() возвращает промис, его удобно использовать вместе с Promise.all().

async function loadPageData() {
  const [userResponse, settingsResponse] = await Promise.all([
    fetch("/api/user"),
    fetch("/api/settings"),
  ]);

  if (!userResponse.ok || !settingsResponse.ok) {
    throw new Error("Не удалось загрузить данные страницы");
  }

  const [user, settings] = await Promise.all([
    userResponse.json(),
    settingsResponse.json(),
  ]);

  console.log(user, settings);
}

Такой подход полезен, когда запросы не зависят друг от друга и их можно запускать одновременно.

fetch() и CORS

fetch() подчиняется правилам безопасности браузера.

Если страница открыта на одном домене, а запрос идёт на другой домен, браузер проверяет CORS-заголовки сервера.

fetch("https://api.learn-javascript.ru/data")

Если сервер не разрешил такой запрос, JavaScript-код не сможет получить ответ.

Важно: fetch() не умеет обходить CORS. Разрешение должно быть настроено на стороне сервера.

Когда это полезно

fetch() используют почти во всех задачах, где фронтенду нужно общаться с сервером:

  • загрузка списка товаров
  • отправка формы обратной связи
  • авторизация пользователя
  • сохранение настроек
  • поиск с параметрами в URL
  • загрузка HTML, текста или файлов

Пример поиска:

async function searchProducts(query) {
  const url = new URL("/api/search", window.location.origin);
  url.searchParams.set("q", query);

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`Ошибка HTTP: ${response.status}`);
  }

  return await response.json();
}

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

Думать, что fetch() сразу возвращает данные

const users = fetch("/api/users");

console.log(users); // Promise

Нужно дождаться промиса и отдельно прочитать тело ответа.

const response = await fetch("/api/users");
const users = await response.json();

Забывать await перед response.json()

const response = await fetch("/api/users");
const users = response.json();

console.log(users); // Promise

response.json() тоже асинхронный метод.

Правильно:

const response = await fetch("/api/users");
const users = await response.json();

console.log(users);

Ожидать, что 404 попадёт в catch автоматически

try {
  const response = await fetch("/api/missing");
  console.log(response.status); // 404
} catch (error) {
  console.log("Ошибка");
}

Для HTTP-статусов проверяйте response.ok.

const response = await fetch("/api/missing");

if (!response.ok) {
  throw new Error(`Ошибка HTTP: ${response.status}`);
}

Читать тело ответа два раза

const response = await fetch("/api/user");

const user = await response.json();
const text = await response.text(); // ошибка

Тело ответа - это поток данных. После чтения оно считается использованным.

Отправлять объект в body без JSON.stringify()

fetch("/api/users", {
  method: "POST",
  body: { name: "Анна" }, // ошибка логики
});

Обычно нужно так:

fetch("/api/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Анна" }),
});

Передавать body в GET-запрос

Для GET данные обычно передают через параметры URL.

const url = new URL("/api/search", window.location.origin);
url.searchParams.set("q", "javascript");

fetch(url);

Если нужно отправить тело запроса, обычно используют POST, PUT или PATCH.

Пытаться обойти CORS на фронтенде

fetch("https://api.learn-javascript.ru/data");

Если сервер не разрешает запросы с вашего сайта, браузер заблокирует доступ к ответу. Это не исправляется специальной настройкой fetch() на фронтенде.

Быстрая памятка

  • fetch() отправляет HTTP-запрос
  • функция возвращает Promise
  • результатом успешного промиса будет объект Response
  • тело ответа нужно читать отдельно через json(), text() и другие методы
  • response.json() тоже возвращает промис
  • HTTP-статусы 404 и 500 сами по себе не переводят промис в catch
  • для проверки HTTP-ошибок используйте response.ok
  • объект в body обычно нужно превращать в JSON через JSON.stringify()
  • тело ответа можно прочитать только один раз
  • отмена запроса делается через AbortController
  • fetch() не обходит CORS-ограничения браузера

Коротко

fetch() - это встроенная функция JavaScript для сетевых запросов. Она возвращает промис с объектом Response, а данные из ответа нужно читать отдельно. В реальном коде важно проверять response.ok, помнить, что response.json() тоже асинхронный, правильно отправлять headers и body, а для отмены долгих или уже ненужных запросов использовать AbortController.