Пошук уроків, статей та іншого контенту
Створите надійну обробку мережевих збоїв, помилок статусів і некоректних відповідей API.
fetchfetch() повертає проміс, який завершується об’єктом Response. Важливо розрізняти два типи проблем:
Мережеві помилки — проміс відхиляється (reject).
Помилки HTTP-статусу — проміс успішно завершується, але response.ok має значення false.
Наприклад, статуси 404 або 500 не спричиняють автоматичний перехід у catch:
const response = await fetch("/api/users/999");
console.log(response.status); // 404
console.log(response.ok); // falseТому перевірку response.ok потрібно виконувати самостійно.
До мережевих проблем належать:
відсутність інтернет-з’єднання;
недоступний сервер;
помилка DNS;
заблокований браузером CORS-запит;
скасування запиту через AbortController.
У таких випадках fetch зазвичай відхиляє проміс:
try {
const response = await fetch("/api/users");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Не вдалося виконати запит:", error);
}Однак цей код не обробляє статуси 400, 404 або 500. Для цього потрібна додаткова перевірка.
Властивість response.ok дорівнює true для статусів від 200 до 299.
async function loadUser() {
const response = await fetch("/api/users/1");
if (!response.ok) {
throw new Error(`Сервер повернув статус ${response.status}`);
}
return response.json();
}
loadUser()
.then((user) => {
console.log("Користувач:", user);
})
.catch((error) => {
console.error("Помилка:", error.message);
});Такий підхід перетворює помилковий HTTP-статус на звичайну помилку JavaScript, яку можна обробити в catch.
API часто повертає опис проблеми в тілі відповіді. Його варто прочитати до створення помилки:
async function request(url) {
const response = await fetch(url);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`HTTP ${response.status}: ${errorText || "без опису помилки"}`
);
}
return response.json();
}Не слід безумовно викликати response.json() для помилкової відповіді. Сервер може повернути HTML, звичайний текст або порожнє тіло.
Навіть якщо статус відповіді успішний, її тіло може містити некоректний JSON:
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);
}Метод response.json() сам генерує помилку, якщо тіло не є коректним JSON. Це потрібно враховувати під час обробки помилок.
Іноді корисно спочатку прочитати відповідь як текст, а потім виконати JSON.parse() самостійно. Так можна додати зрозуміліше повідомлення:
async function parseJsonResponse(response) {
const text = await response.text();
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch {
throw new Error("Сервер повернув некоректний JSON");
}
}fetch не має вбудованого тайм-ауту. Якщо сервер перестав відповідати, запит може очікувати дуже довго.
Для обмеження часу використовується AbortController:
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeoutMs);
try {
return await fetch(url, {
signal: controller.signal
});
} catch (error) {
if (error.name === "AbortError") {
throw new Error(`Запит перевищив ліміт у ${timeoutMs} мс`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}Блок finally виконується незалежно від результату запиту. Він потрібен, щоб таймер не залишався активним після успішної відповіді або іншої помилки.
Повторювати запит можна не для всіх помилок.
Зазвичай повтор доречний для:
408 Request Timeout;
429 Too Many Requests;
500 Internal Server Error;
502 Bad Gateway;
503 Service Unavailable;
504 Gateway Timeout;
тимчасових мережевих помилок.
Не варто автоматично повторювати запити після 400, 401, 403 або 404: повторна спроба зазвичай не змінить результат.
Між спробами корисно використовувати затримку, яка збільшується:
function delay(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}Нижче наведено приклад функції, яка:
перевіряє HTTP-статус;
обробляє мережеві помилки;
має тайм-аут;
перевіряє формат відповіді;
повторює тимчасові помилки;
повертає зрозумілі повідомлення про проблеми.
class ApiError extends Error {
constructor(message, status = null) {
super(message);
this.name = "ApiError";
this.status = status;
}
}
function delay(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
function shouldRetryStatus(status) {
return [408, 429, 500, 502, 503, 504].includes(status);
}
async function fetchJson(
url,
{
timeoutMs = 5000,
retries = 2,
...options
} = {}
) {
for (let attempt = 0; attempt <= retries; attempt += 1) {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorBody = await response.text();
const message =
errorBody.trim() ||
`Сервер повернув статус ${response.status}`;
const error = new ApiError(message, response.status);
if (!shouldRetryStatus(response.status) || attempt === retries) {
throw error;
}
// Чекаємо довше перед кожною наступною спробою
await delay(500 * 2 ** attempt);
continue;
}
if (response.status === 204) {
return null;
}
const contentType = response.headers.get("content-type") || "";
if (!contentType.includes("application/json")) {
throw new ApiError(
"Сервер повернув відповідь не у форматі JSON",
response.status
);
}
const responseText = await response.text();
if (!responseText.trim()) {
return null;
}
try {
return JSON.parse(responseText);
} catch {
throw new ApiError("Сервер повернув некоректний JSON");
}
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof ApiError) {
throw error;
}
if (error.name === "AbortError") {
if (attempt === retries) {
throw new Error(
`Запит не завершився за ${timeoutMs} мс`
);
}
// Тайм-аут може бути тимчасовою проблемою, тому повторюємо запит
await delay(500 * 2 ** attempt);
continue;
}
if (attempt === retries) {
throw new Error(
`Мережева помилка: ${error.message}`
);
}
// Повторюємо запит після тимчасової мережевої помилки
await delay(500 * 2 ** attempt);
}
}
throw new Error("Не вдалося отримати відповідь від сервера");
}
async function loadTodo() {
try {
const todo = await fetchJson(
"https://jsonplaceholder.typicode.com/todos/1",
{
timeoutMs: 3000,
retries: 2,
headers: {
Accept: "application/json"
}
}
);
console.log("Отримані дані:", todo);
} catch (error) {
if (error instanceof ApiError && error.status === 404) {
console.error("Ресурс не знайдено");
return;
}
if (error instanceof ApiError) {
console.error(
`Помилка API (${error.status ?? "без статусу"}):`,
error.message
);
return;
}
console.error("Непередбачена помилка:", error.message);
}
}
loadTodo();Функція приймає додаткові опції fetch, тому її можна використовувати для різних HTTP-методів:
await fetchJson("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify({
name: "Олена",
email: "olena@example.com"
})
});Помилку потрібно не лише записати в консоль. Користувач має отримати зрозумілий стан інтерфейсу:
показати індикатор завантаження під час запиту;
відобразити повідомлення про проблему;
надати кнопку повторної спроби;
приховати індикатор після завершення запиту.
const statusElement = document.querySelector("#status");
const button = document.querySelector("#load-button");
async function loadData() {
statusElement.textContent = "Завантаження...";
button.disabled = true;
try {
const data = await fetchJson("/api/profile");
statusElement.textContent = `Вітаємо, ${data.name}!`;
} catch (error) {
if (error instanceof ApiError && error.status === 401) {
statusElement.textContent = "Потрібно увійти в систему.";
} else if (error instanceof ApiError) {
statusElement.textContent = "Сервер повернув помилку.";
} else {
statusElement.textContent =
"Не вдалося завантажити дані. Спробуйте ще раз.";
}
} finally {
button.disabled = false;
}
}
button.addEventListener("click", loadData);Технічні деталі помилки краще залишати для журналу або системи моніторингу, а користувачу показувати короткий і зрозумілий текст.
Окрім автоматичного тайм-ауту, запит можна скасувати вручну. Наприклад, коли користувач залишив сторінку або почав новий пошук:
let currentController = null;
async function searchUsers(query) {
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
try {
const response = await fetch(
`/api/users?search=${encodeURIComponent(query)}`,
{
signal: currentController.signal
}
);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
// Попередній запит більше не потрібен
return null;
}
throw error;
}
}Це особливо важливо для пошуку під час введення тексту: старі запити можуть завершитися пізніше за нові й перезаписати актуальні результати.
Для надійного запиту зручно дотримуватися такого порядку:
Створити AbortController і таймер.
Виконати fetch.
Обробити мережеві помилки через try...catch.
Перевірити response.ok.
Для помилкового статусу прочитати тіло відповіді.
Перевірити Content-Type.
Розібрати JSON і обробити помилку парсингу.
Повторити лише тимчасові помилки.
У finally очистити ресурси та оновити стан інтерфейсу.
404 автоматично потрапить у catchtry {
const response = await fetch("/api/missing-resource");
console.log("Цей код може виконатися навіть для 404");
} catch {
console.log("Сюди потрапляють переважно мережеві помилки");
}Потрібно явно перевіряти response.ok.
response.json() без перевірки статусуПомилкова відповідь може бути HTML або звичайним текстом. У такому разі response.json() завершиться помилкою парсингу й приховає справжню причину проблеми.
Безумовні повтори можуть:
створити зайве навантаження на сервер;
багато разів виконати небезпечну операцію;
затримати повідомлення про помилку;
погіршити ситуацію під час недоступності сервера.
Повторюйте лише тимчасові помилки й обмежуйте кількість спроб.
POST-запитівПовторний POST може створити дубльований ресурс або повторно виконати операцію. Для таких запитів потрібно враховувати ідемпотентність операції та можливості API.
Без тайм-ауту інтерфейс може залишатися в стані завантаження невизначено довго. Для важливих запитів використовуйте AbortController.
204 No ContentСтатус 204 означає успішну відповідь без тіла. Виклик response.json() у такому випадку спричинить помилку. Перед розбором потрібно перевірити статус або порожнє тіло.
Тексти на кшталт Failed to fetch або повний стек помилки не завжди зрозумілі користувачу. Розділяйте:
технічну інформацію для журналу;
коротке повідомлення для інтерфейсу.
fetch не вважає статуси 4xx і 5xx JavaScript-помилками.
Для перевірки HTTP-результату використовуйте response.ok або response.status.
Мережеві помилки й помилки парсингу обробляйте через try...catch.
Не припускайте, що кожна відповідь містить коректний JSON.
Для довгих запитів використовуйте AbortController і тайм-аут.
Повторюйте лише тимчасові помилки та обмежуйте кількість спроб.
Очищайте таймери й оновлюйте стан інтерфейсу в finally.
Користувачу показуйте зрозуміле повідомлення, а технічні деталі зберігайте окремо.