Пошук уроків, статей та іншого контенту
Обробите HTTP-статуси й мережеві збої, враховуючи, що fetch() не відхиляє проміс через статус 4xx або 5xx.
fetch() обробляє помилкиfetch() повертає проміс, який виконується об’єктом Response, якщо браузер отримав відповідь від сервера.
Важливо: HTTP-статус сам по собі не вважається помилкою для fetch().
Тобто проміс виконається успішно навіть для таких відповідей:
404 Not Found;
401 Unauthorized;
403 Forbidden;
500 Internal Server Error.
Проміс буде відхилено переважно тоді, коли запит не вдалося виконати на мережевому рівні:
немає підключення до мережі;
сервер недоступний;
запит заблоковано політикою CORS;
запит скасовано через AbortController.
Тому перевіряти лише catch() недостатньо. Потрібно також перевіряти властивість response.ok або значення response.status.
response.okВластивість response.ok має значення:
true, якщо HTTP-статус у діапазоні від 200 до 299;
false для інших статусів.
fetch("/api/profile")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
return response.json();
})
.then((profile) => {
console.log("Профіль:", profile);
})
.catch((error) => {
console.error("Не вдалося отримати профіль:", error);
});Якщо сервер поверне 404, response.json() не буде викликано, а виконання перейде до catch() через створену нами помилку.
Іноді різні статуси потребують різної поведінки. Наприклад, 401 означає, що користувач не авторизований, а 404 — що ресурс не знайдено.
async function loadProduct(productId) {
const response = await fetch(`/api/products/${productId}`);
if (response.status === 404) {
return null;
}
if (response.status === 401) {
throw new Error("Потрібно увійти в обліковий запис");
}
if (!response.ok) {
throw new Error(`Сервер повернув статус ${response.status}`);
}
return response.json();
}
loadProduct(42)
.then((product) => {
if (product === null) {
console.log("Товар не знайдено");
return;
}
console.log("Товар:", product);
})
.catch((error) => {
console.error(error.message);
});Мережевий збій обробляється через try...catch під час використання async/await.
async function loadData() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Помилка запиту:", error.message);
}
}
loadData();У цьому прикладі catch обробляє два типи проблем:
мережеві помилки, через які fetch() не зміг отримати відповідь;
помилки, які ми створили самостійно для HTTP-статусів, що не є успішними.
У застосунку зручно створити окрему функцію, яка:
виконує запит;
перевіряє HTTP-статус;
читає тіло відповіді;
розрізняє мережеві та HTTP-помилки.
async function requestJson(url, options = {}) {
let response;
try {
response = await fetch(url, options);
} catch (error) {
// Запит не дійшов до сервера або був скасований
throw new Error(`Мережева помилка: ${error.message}`);
}
if (!response.ok) {
let serverMessage = "";
try {
const errorData = await response.json();
serverMessage = errorData.message || "";
} catch {
// Тіло відповіді може бути не JSON
}
const details = serverMessage ? `: ${serverMessage}` : "";
throw new Error(
`HTTP-помилка ${response.status} ${response.statusText}${details}`
);
}
try {
return await response.json();
} catch {
throw new Error("Сервер повернув некоректний JSON");
}
}
async function showUser() {
try {
const user = await requestJson("/api/user");
console.log("Користувач:", user);
} catch (error) {
console.error(error.message);
}
}
showUser();Такий підхід централізує обробку помилок. Інші частини програми можуть працювати лише з результатом або перехоплювати вже зрозумілу помилку.
Навіть якщо HTTP-запит успішний, обробка тіла відповіді може завершитися помилкою.
Наприклад:
сервер повернув пошкоджений JSON;
відповідь порожня;
сервер повернув HTML замість JSON;
відповідь має інший формат.
Метод response.json() також повертає проміс, тому його помилка повинна бути оброблена:
async function loadSettings() {
try {
const response = await fetch("/api/settings");
if (!response.ok) {
throw new Error(`Статус відповіді: ${response.status}`);
}
const settings = await response.json();
console.log(settings);
} catch (error) {
console.error("Не вдалося завантажити налаштування:", error.message);
}
}Якщо сервер повертає звичайний текст, замість json() потрібно використати text():
async function loadMessage() {
const response = await fetch("/api/message");
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const message = await response.text();
console.log(message);
}AbortControllerЗапит можна скасувати, наприклад якщо користувач залишив сторінку або перевищено дозволений час очікування.
async function loadWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timerId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
throw new Error("Запит перевищив допустимий час очікування");
}
throw error;
} finally {
clearTimeout(timerId);
}
}
loadWithTimeout("/api/products", 3000)
.then((products) => {
console.log("Товари:", products);
})
.catch((error) => {
console.error(error.message);
});finally виконується незалежно від результату запиту. У цьому прикладі він очищає таймер після успішної відповіді, HTTP-помилки або мережевого збою.
Помилку потрібно не лише вивести в консоль, а й показати користувачеві зрозуміле повідомлення.
const button = document.querySelector("#load-button");
const message = document.querySelector("#message");
button.addEventListener("click", async () => {
button.disabled = true;
message.textContent = "Завантаження...";
try {
const response = await fetch("/api/news");
if (!response.ok) {
if (response.status === 404) {
throw new Error("Новини не знайдено");
}
if (response.status >= 500) {
throw new Error("Сервер тимчасово недоступний");
}
throw new Error(`Не вдалося завантажити новини: ${response.status}`);
}
const news = await response.json();
message.textContent = `Завантажено новин: ${news.length}`;
} catch (error) {
message.textContent = error.message;
} finally {
button.disabled = false;
}
});Очікувана HTML-структура для цього прикладу:
<button id="load-button">Завантажити новини</button>
<p id="message"></p>Повторення може бути доречним для тимчасових мережевих проблем або статусів 5xx. Не варто бездумно повторювати всі запити:
повторення 404 зазвичай не допоможе;
повторення 401 не авторизує користувача;
повторення POST може створити дубльовані дані;
надто багато повторів створюють додаткове навантаження.
Приклад обмеженого повторення для запиту:
function wait(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
async function requestWithRetry(url, options = {}, attempts = 3) {
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
if (response.status < 500) {
throw new Error(`Запит завершився зі статусом ${response.status}`);
}
if (attempt === attempts) {
throw new Error(`Сервер недоступний: ${response.status}`);
}
} catch (error) {
if (attempt === attempts) {
throw error;
}
await wait(attempt * 1000);
}
}
}
async function loadProducts() {
try {
const response = await requestWithRetry("/api/products");
const products = await response.json();
console.log(products);
} catch (error) {
console.error("Остаточна помилка:", error.message);
}
}
loadProducts();У реальному застосунку стратегію повторення потрібно узгоджувати з типом операції та правилами API.
fetch() сам відхилить проміс для 404fetch("/missing-resource")
.catch(() => {
console.log("Цей обробник може не виконатися");
});Для 404 проміс зазвичай буде виконано. Потрібно перевірити response.ok:
fetch("/missing-resource")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
return response.json();
})
.catch((error) => {
console.error(error.message);
});response.json() до перевірки статусуЯкщо сервер повернув HTML-сторінку помилки або порожню відповідь, json() може завершитися помилкою, яка приховає справжній HTTP-статус.
Краще спочатку перевірити response.ok, а потім читати тіло відповіді.
Успішний статус 200 не гарантує, що тіло відповіді є коректним JSON. Виклик response.json() також потрібно виконувати в контексті обробки помилок.
Повідомлення на кшталт Failed to fetch малоінформативне для користувача. У програмі можна записати технічні деталі в консоль, а в інтерфейсі показати зрозумілий текст:
console.error(error);
message.textContent = "Не вдалося завантажити дані. Спробуйте ще раз.";finally для відновлення стану інтерфейсуЯкщо кнопка вимкнена на час запиту, її потрібно знову ввімкнути і після успіху, і після помилки. Для цього зручно використовувати finally.
fetch() не вважає статуси 4xx і 5xx мережевими помилками.
Для перевірки HTTP-результату використовуйте response.ok або response.status.
Мережеві помилки обробляйте через catch або try...catch.
Перед викликом response.json() перевіряйте статус відповіді.
Помилка може виникнути і під час читання або розбору тіла відповіді.
AbortController дає змогу скасувати запит і реалізувати тайм-аут.
finally корисний для очищення ресурсів і відновлення стану інтерфейсу.
Повторюйте запити обмежено та лише для помилок, які можуть бути тимчасовими.