Пошук уроків, статей та іншого контенту
Виконуватимете HTTP-запити через fetch і оброблятимете відповіді, помилки та JSON.
Fetch API — вбудований браузерний API для виконання мережевих HTTP-запитів. За допомогою fetch() можна отримувати дані із сервера, надсилати форми, створювати, оновлювати та видаляти ресурси.
Найпростіший запит має такий вигляд:
fetch("/api/products");Функція fetch() одразу повертає Promise. Спочатку цей проміс містить об’єкт Response, а самі дані потрібно прочитати окремо.
fetch("/api/products")
.then((response) => response.json())
.then((products) => {
console.log(products);
});Метод response.json() також повертає проміс, оскільки читання та розбір тіла відповіді відбуваються асинхронно.
async і awaitНа практиці часто використовують async/await. Такий синтаксис робить послідовність асинхронних операцій зрозумілішою:
async function loadProducts() {
const response = await fetch("/api/products");
const products = await response.json();
console.log(products);
}
loadProducts();await можна використовувати лише всередині async-функції або на верхньому рівні JavaScript-модуля.
ResponseПісля виконання запиту fetch() повертає об’єкт Response. Він містить інформацію про HTTP-відповідь:
async function loadData() {
const response = await fetch("/api/products");
console.log(response.status); // Наприклад, 200
console.log(response.statusText); // Наприклад, "OK"
console.log(response.ok); // true або false
console.log(response.headers);
}Найчастіше використовують такі властивості:
status — числовий HTTP-статус;
statusText — текстовий опис статусу;
ok — true, якщо статус належить діапазону 200–299;
headers — заголовки відповіді;
url — фактична URL-адреса відповіді.
Залежно від формату даних використовують різні методи:
const response = await fetch("/data");
const jsonData = await response.json();
const textData = await response.text();
const blobData = await response.blob();
const arrayBufferData = await response.arrayBuffer();Тіло відповіді можна прочитати лише один раз. Наприклад, після response.json() не можна повторно викликати response.text() для того самого об’єкта Response.
const response = await fetch("/data.json");
const data = await response.json();
// Помилка: тіло відповіді вже прочитане
const text = await response.text();Важлива особливість fetch() полягає в тому, що він зазвичай не відхиляє проміс через HTTP-помилки.
Наприклад, відповідь зі статусом 404 або 500 все одно буде успішно отримана з точки зору мережевого запиту. Тому потрібно самостійно перевіряти response.ok.
async function loadUser() {
const response = await fetch("/api/users/1");
if (!response.ok) {
throw new Error(`Помилка HTTP: ${response.status}`);
}
return response.json();
}Якщо не перевіряти response.ok, програма може спробувати обробити сторінку помилки як звичайні дані.
Для обробки помилок у async-функціях використовують try...catch:
async function loadUser() {
try {
const response = await fetch("/api/users/1");
if (!response.ok) {
throw new Error(`Сервер повернув статус ${response.status}`);
}
const user = await response.json();
console.log(user);
} catch (error) {
console.error("Не вдалося завантажити користувача:", error);
}
}
loadUser();catch може спрацювати в таких випадках:
немає мережевого з’єднання;
запит заблоковано політикою CORS;
запит скасовано;
під час обробки виникла помилка;
вручну було виконано throw.
HTTP-статуси 404, 401 або 500 самі по собі не спричиняють перехід у catch. Для них потрібно перевіряти response.ok.
За замовчуванням fetch() виконує запит методом GET.
const response = await fetch("/api/products");Метод можна змінити другим аргументом — об’єктом налаштувань:
const response = await fetch("/api/products/10", {
method: "DELETE"
});Поширені HTTP-методи:
GET — отримати дані;
POST — створити ресурс або надіслати дані;
PUT — повністю замінити ресурс;
PATCH — частково оновити ресурс;
DELETE — видалити ресурс.
Заголовки передаються у властивості headers:
const response = await fetch("/api/products", {
headers: {
Accept: "application/json",
Authorization: "Bearer token-value"
}
});Заголовок Accept повідомляє серверу, який формат відповіді бажано отримати.
Якщо надсилається JSON, зазвичай потрібно вказати заголовок Content-Type:
const response = await fetch("/api/products", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Keyboard",
price: 120
})
});Метод JSON.stringify() перетворює JavaScript-об’єкт на JSON-рядок:
const product = {
name: "Mouse",
price: 80
};
const response = await fetch("/api/products", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify(product)
});
if (!response.ok) {
throw new Error(`Не вдалося створити товар: ${response.status}`);
}
const createdProduct = await response.json();
console.log(createdProduct);Важливо не передавати об’єкт напряму:
// Неправильно: body очікує рядок, FormData, Blob та інші підтримувані типи
fetch("/api/products", {
method: "POST",
body: {
name: "Mouse"
}
});Правильний варіант:
fetch("/api/products", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Mouse"
})
});Для формування URL із параметрами зручно використовувати URL і URLSearchParams:
const url = new URL("/api/products", window.location.origin);
url.searchParams.set("category", "electronics");
url.searchParams.set("limit", "10");
const response = await fetch(url);У результаті URL може мати такий вигляд:
/api/products?category=electronics&limit=10Такий підхід безпечніший і надійніший за ручне об’єднання рядків, особливо коли значення містять пробіли або спеціальні символи.
Нижче наведено сторінку, яка завантажує один тестовий ресурс, показує стан завантаження та обробляє помилки.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Fetch API</title>
</head>
<body>
<button id="load-button">Завантажити допис</button>
<p id="status"></p>
<article id="post"></article>
<script>
const loadButton = document.querySelector("#load-button");
const statusElement = document.querySelector("#status");
const postElement = document.querySelector("#post");
async function loadPost() {
loadButton.disabled = true;
statusElement.textContent = "Завантаження...";
postElement.textContent = "";
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts/1",
{
headers: {
Accept: "application/json"
}
}
);
// fetch не вважає статус 404 або 500 мережевою помилкою
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const post = await response.json();
postElement.innerHTML = `
<h2>${escapeHtml(post.title)}</h2>
<p>${escapeHtml(post.body)}</p>
`;
statusElement.textContent = "Дані завантажено";
} catch (error) {
statusElement.textContent = "Не вдалося завантажити дані";
console.error(error);
} finally {
loadButton.disabled = false;
}
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
loadButton.addEventListener("click", loadPost);
</script>
</body>
</html>Функція escapeHtml() у прикладі захищає вставлення отриманого тексту в innerHTML. Якщо дані не потрібно вставляти як HTML, безпечніше використовувати textContent.
Якщо кілька незалежних запитів можна виконувати одночасно, використовуйте Promise.all():
async function loadPageData() {
const [productsResponse, categoriesResponse] = await Promise.all([
fetch("/api/products"),
fetch("/api/categories")
]);
if (!productsResponse.ok || !categoriesResponse.ok) {
throw new Error("Не вдалося завантажити дані сторінки");
}
const [products, categories] = await Promise.all([
productsResponse.json(),
categoriesResponse.json()
]);
return { products, categories };
}Послідовний варіант був би повільнішим:
const productsResponse = await fetch("/api/products");
const categoriesResponse = await fetch("/api/categories");У цьому випадку другий запит почнеться лише після завершення першого. Послідовне виконання потрібне тоді, коли другий запит залежить від результату першого.
Для скасування запиту використовують AbortController:
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, 5000);
try {
const response = await fetch("/api/products", {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const products = await response.json();
console.log(products);
} catch (error) {
if (error.name === "AbortError") {
console.log("Запит скасовано або перевищено час очікування");
} else {
console.error("Помилка запиту:", error);
}
}Скасування корисне для:
пошукових запитів під час введення тексту;
запитів, що більше не потрібні після переходу на іншу сторінку;
обмеження часу очікування відповіді;
запобігання зайвим мережевим операціям.
Якщо сторінка виконує запит до іншого походження, браузер застосовує політику CORS — Cross-Origin Resource Sharing.
Походження визначається комбінацією:
протоколу;
домену;
порту.
Наприклад, запит із https://app.example.com до https://api.example.com є міждоменним, навіть якщо обидві адреси мають той самий основний домен.
Сервер має дозволити такий доступ за допомогою відповідних заголовків, зокрема:
Access-Control-Allow-OriginВажливо:
CORS налаштовується на сервері;
додавання довільного заголовка у fetch() не вимикає CORS;
режим no-cors не є способом отримати звичайну доступну для читання відповідь;
у локальній розробці сторінку краще запускати через HTTP-сервер, а не відкривати безпосередньо через file://.
За замовчуванням cookies для міждоменних запитів не надсилаються. Для запиту до того самого походження cookies зазвичай надсилаються автоматично.
Явно вказати режим можна так:
const response = await fetch("/api/profile", {
credentials: "same-origin"
});Для міждоменного запиту потрібно:
const response = await fetch("https://api.example.com/profile", {
credentials: "include"
});У такому разі сервер також має правильно налаштувати CORS і дозволити credentials. Не можна використовувати Access-Control-Allow-Origin: * разом із credentials.
Для надсилання HTML-форми можна використовувати FormData:
const form = document.querySelector("#profile-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) {
throw new Error(`Помилка збереження: ${response.status}`);
}
});Для FormData не потрібно вручну встановлювати Content-Type. Браузер сам додасть правильний заголовок разом із boundary, необхідним для multipart-запиту.
Повторювану перевірку статусу можна винести в окрему функцію:
async function requestJson(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
Accept: "application/json",
...options.headers
}
});
if (!response.ok) {
let message = `HTTP-помилка: ${response.status}`;
try {
const errorData = await response.json();
if (errorData.message) {
message = errorData.message;
}
} catch {
// Відповідь може бути не у форматі JSON
}
throw new Error(message);
}
return response.json();
}
async function createProduct() {
const product = await requestJson("/api/products", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Monitor",
price: 300
})
});
console.log(product);
}Тепер у різних частинах застосунку не потрібно дублювати перевірку response.ok і перетворення відповіді на JSON.
response.okconst response = await fetch("/api/products");
const data = await response.json();Якщо сервер поверне 500, код все одно спробує обробити відповідь. Перевіряйте статус перед читанням даних:
if (!response.ok) {
throw new Error(`Помилка: ${response.status}`);
}fetch() відхилить проміс для статусу 404fetch() відхиляє проміс переважно через мережеві проблеми або скасування запиту. Статуси 4xx і 5xx потрібно перевіряти самостійно.
await для response.json()const data = response.json();
console.log(data); // PromiseПравильно:
const data = await response.json();const data = await response.json();
const text = await response.text(); // ПомилкаВиберіть один метод читання тіла відповіді.
JSON.stringify()Для JSON-тіла використовуйте і JSON.stringify(), і заголовок Content-Type: application/json.
Promise.all()Якщо один запит залежить від результату іншого, їх не можна безпосередньо виконувати паралельно:
const userResponse = await fetch("/api/user");
const user = await userResponse.json();
const ordersResponse = await fetch(`/api/users/${user.id}/orders`);У цьому випадку URL другого запиту залежить від user.id.
Перевіряйте response.ok після кожного HTTP-запиту.
Обробляйте як мережеві помилки, так і помилкові HTTP-статуси.
Перетворюйте JSON через response.json() лише один раз.
Для незалежних запитів використовуйте Promise.all().
Для тривалих або непотрібних запитів розглядайте AbortController.
Не довіряйте даним із сервера під час вставлення їх у innerHTML.
Для запитів із JSON явно задавайте Content-Type.
Не намагайтеся виправити серверні CORS-проблеми лише налаштуваннями клієнтського коду.
fetch() виконує HTTP-запити та повертає Promise.
Результат fetch() — це об’єкт Response, а не готові дані.
JSON читають за допомогою await response.json().
HTTP-помилки потрібно перевіряти через response.ok або response.status.
Метод, заголовки та тіло запиту передаються другим аргументом fetch().
JSON у тілі запиту потрібно перетворювати через JSON.stringify().
Незалежні запити можна виконувати паралельно за допомогою Promise.all().
Запити можна скасовувати через AbortController.
Міждоменні запити контролюються політикою CORS, яку має дозволити сервер.