Пошук уроків, статей та іншого контенту
Розберете проміси, async/await і послідовне виконання кількох запитів до сервера.
JavaScript виконує код в одному основному потоці. Це означає, що довга синхронна операція може заблокувати виконання іншого коду.
Мережеві запити, таймери та читання файлів можуть тривати невизначений час. Тому JavaScript не зупиняє всю програму в очікуванні відповіді сервера. Замість цього він запускає операцію й продовжує виконання, а результат буде оброблено пізніше.
console.log("Початок");
setTimeout(() => {
console.log("Результат асинхронної операції");
}, 1000);
console.log("Кінець");Результат:
Початок
Кінець
Результат асинхронної операціїАсинхронний код не виконується обов’язково в тому порядку, у якому його записано. Порядок залежить від завершення операцій.
Promise — це об’єкт, який представляє майбутній результат асинхронної операції.
Проміс може перебувати в одному з трьох станів:
pending — операція ще виконується;
fulfilled — операція успішно завершилася;
rejected — операція завершилася помилкою.
Наприклад:
const promise = new Promise((resolve, reject) => {
const operationSucceeded = true;
if (operationSucceeded) {
resolve("Операція виконана");
} else {
reject(new Error("Операція завершилася помилкою"));
}
});
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error.message);
});Метод .then() виконується після успішного завершення промісу, а .catch() — якщо сталася помилка.
.then()Кожен виклик .then() повертає новий проміс. Це дає змогу створювати ланцюжки асинхронних операцій:
Promise.resolve(2)
.then((number) => {
return number * 3;
})
.then((number) => {
return number + 1;
})
.then((result) => {
console.log(result); // 7
});Якщо повернути з .then() інший проміс, наступний обробник дочекається його завершення:
function getNumber() {
return new Promise((resolve) => {
setTimeout(() => resolve(10), 500);
});
}
getNumber()
.then((number) => {
return number * 2;
})
.then((result) => {
console.log(result); // 20
});Важливо повертати проміс із callback-функції. Без return ланцюжок втратить зв’язок із наступною асинхронною операцією.
fetchfetch() використовується для HTTP-запитів. Він повертає проміс, який успішно завершується об’єктом Response.
fetch("https://jsonplaceholder.typicode.com/users/1")
.then((response) => {
return response.json();
})
.then((user) => {
console.log(user);
})
.catch((error) => {
console.error("Помилка запиту:", error);
});Метод response.json() також повертає проміс, оскільки тіло відповіді потрібно прочитати й перетворити з JSON на JavaScript-об’єкт.
fetch() відхиляє проміс переважно через мережеві помилки. Наприклад, якщо немає з’єднання з сервером.
HTTP-відповідь зі статусом 404 або 500 сама по собі не вважається помилкою для fetch(). Тому потрібно перевіряти властивість response.ok.
fetch("https://jsonplaceholder.typicode.com/users/9999")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error(error.message);
});response.ok має значення true для успішних статусів HTTP у діапазоні 200–299.
Іноді наступний запит залежить від результату попереднього. Наприклад:
отримати користувача;
отримати його публікації;
отримати коментарі до першої публікації.
У такому випадку запити потрібно виконувати послідовно.
.then()function fetchJson(url) {
return fetch(url).then((response) => {
if (!response.ok) {
throw new Error(`HTTP-помилка ${response.status}: ${url}`);
}
return response.json();
});
}
fetchJson("https://jsonplaceholder.typicode.com/users/1")
.then((user) => {
console.log("Користувач:", user.name);
return fetchJson(
`https://jsonplaceholder.typicode.com/users/${user.id}/posts`
);
})
.then((posts) => {
console.log("Кількість публікацій:", posts.length);
if (posts.length === 0) {
throw new Error("У користувача немає публікацій");
}
const firstPost = posts[0];
return fetchJson(
`https://jsonplaceholder.typicode.com/posts/${firstPost.id}/comments`
);
})
.then((comments) => {
console.log("Коментарі до першої публікації:", comments);
})
.catch((error) => {
console.error("Не вдалося завантажити дані:", error.message);
});У цьому прикладі:
другий запит починається після отримання користувача;
третій запит починається після отримання списку публікацій;
будь-яка помилка переходить до спільного .catch().
Правильний ланцюжок:
fetch("/api/user")
.then((response) => {
return response.json();
})
.then((user) => {
return fetch(`/api/orders?userId=${user.id}`);
})
.then((response) => {
return response.json();
})
.then((orders) => {
console.log(orders);
});Якщо не написати return, наступний .then() може виконатися раніше, ніж завершиться запит:
fetch("/api/user")
.then((response) => {
response.json(); // Проміс не повернуто
})
.then((user) => {
// user матиме значення undefined
console.log(user);
});async і awaitСинтаксис async/await є зручнішим способом роботи з промісами.
async перед функцією означає, що функція завжди повертає проміс;
await призупиняє виконання цієї асинхронної функції до завершення промісу;
await можна використовувати всередині функції, оголошеної з async.
Той самий ланцюжок можна переписати так:
async function loadUserData() {
try {
const userResponse = await fetch(
"https://jsonplaceholder.typicode.com/users/1"
);
if (!userResponse.ok) {
throw new Error(`Помилка користувача: ${userResponse.status}`);
}
const user = await userResponse.json();
console.log("Користувач:", user.name);
const postsResponse = await fetch(
`https://jsonplaceholder.typicode.com/users/${user.id}/posts`
);
if (!postsResponse.ok) {
throw new Error(`Помилка публікацій: ${postsResponse.status}`);
}
const posts = await postsResponse.json();
console.log("Кількість публікацій:", posts.length);
if (posts.length === 0) {
return {
user,
posts,
comments: [],
};
}
const commentsResponse = await fetch(
`https://jsonplaceholder.typicode.com/posts/${posts[0].id}/comments`
);
if (!commentsResponse.ok) {
throw new Error(`Помилка коментарів: ${commentsResponse.status}`);
}
const comments = await commentsResponse.json();
return {
user,
posts,
comments,
};
} catch (error) {
console.error("Не вдалося завантажити дані:", error.message);
throw error;
}
}
loadUserData()
.then((data) => {
console.log("Усі дані завантажено:", data);
})
.catch(() => {
console.log("Потрібно показати повідомлення про помилку користувачу");
});await не блокує весь JavaScript і браузер. Він призупиняє лише виконання поточної async-функції, дозволяючи іншим завданням продовжувати роботу.
У кожному запиті потрібно:
викликати fetch();
перевірити response.ok;
перетворити тіло відповіді через response.json().
Цю логіку зручно винести в окрему функцію:
async function fetchJson(url, options = {}) {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP-помилка ${response.status}`);
}
return response.json();
}
async function loadData() {
const user = await fetchJson(
"https://jsonplaceholder.typicode.com/users/1"
);
const posts = await fetchJson(
`https://jsonplaceholder.typicode.com/users/${user.id}/posts`
);
return {
user,
posts,
};
}
loadData()
.then(({ user, posts }) => {
console.log(`${user.name} має ${posts.length} публікацій`);
})
.catch((error) => {
console.error("Помилка:", error.message);
});Така функція може працювати не лише з GET-запитами, а й з іншими методами:
async function createPost() {
const post = await fetchJson(
"https://jsonplaceholder.typicode.com/posts",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Нова публікація",
body: "Текст публікації",
userId: 1,
}),
}
);
console.log("Створено публікацію:", post);
}
createPost().catch((error) => {
console.error(error.message);
});try...catchУ синхронному коді помилки обробляють через try...catch. Такий самий підхід працює з await.
async function showUser() {
try {
const user = await fetchJson(
"https://jsonplaceholder.typicode.com/users/1"
);
console.log(user.name);
} catch (error) {
console.error("Не вдалося отримати користувача:", error.message);
}
}
showUser();Якщо функція не може самостійно обробити помилку, її можна передати вище через throw:
async function getUser() {
try {
return await fetchJson(
"https://jsonplaceholder.typicode.com/users/1"
);
} catch (error) {
throw new Error(`Помилка завантаження користувача: ${error.message}`);
}
}
async function main() {
try {
const user = await getUser();
console.log(user);
} catch (error) {
console.error(error.message);
}
}
main();Оберіть місце обробки помилки залежно від відповідальності функції:
функція доступу до API може додати контекст і передати помилку далі;
функція інтерфейсу може показати повідомлення користувачу;
логіка застосунку може повторити запит або використати резервне значення.
Послідовне виконання потрібне, коли один запит залежить від результату іншого:
const user = await fetchJson("/api/user/1");
const posts = await fetchJson(`/api/posts?userId=${user.id}`);Другий запит не можна почати раніше, бо для його URL потрібен user.id.
Якщо запити незалежні, не потрібно виконувати їх послідовно. У такому випадку їх можна запустити одночасно за допомогою Promise.all():
async function loadDashboard() {
const [users, posts] = await Promise.all([
fetchJson("https://jsonplaceholder.typicode.com/users"),
fetchJson("https://jsonplaceholder.typicode.com/posts"),
]);
return {
users,
posts,
};
}
loadDashboard()
.then((dashboard) => {
console.log("Користувачів:", dashboard.users.length);
console.log("Публікацій:", dashboard.posts.length);
})
.catch((error) => {
console.error("Не вдалося завантажити панель:", error.message);
});У цьому прикладі обидва запити стартують майже одночасно. Promise.all():
повертає результати в тому самому порядку, що й передані проміси;
завершується успішно, лише якщо успішно завершилися всі проміси;
завершується з помилкою, якщо хоча б один проміс відхилено.
Використовуйте послідовний await, якщо:
наступний запит залежить від результату попереднього;
порядок виконання має значення;
потрібно зупинити сценарій після певної помилки.
Використовуйте Promise.all(), якщо:
запити незалежні;
потрібно зменшити загальний час очікування;
помилка одного запиту має зупинити отримання всіх результатів.
Іноді потрібно отримати всі результати, навіть якщо частина запитів завершилася помилкою. Для цього використовують Promise.allSettled().
async function loadSeveralResources() {
const results = await Promise.allSettled([
fetchJson("https://jsonplaceholder.typicode.com/users"),
fetchJson("https://jsonplaceholder.typicode.com/posts"),
fetchJson("https://jsonplaceholder.typicode.com/invalid-resource"),
]);
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`Запит ${index + 1} успішний`);
} else {
console.error(`Запит ${index + 1} завершився помилкою`);
}
});
}
loadSeveralResources();На відміну від Promise.all(), Promise.allSettled() дочекається завершення кожного промісу.
fetch() не має вбудованого універсального тайм-ауту. Для скасування запиту можна використати AbortController.
async function fetchJsonWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = 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();
} finally {
clearTimeout(timeoutId);
}
}
fetchJsonWithTimeout(
"https://jsonplaceholder.typicode.com/users/1",
5000
)
.then((user) => {
console.log(user);
})
.catch((error) => {
if (error.name === "AbortError") {
console.error("Запит перевищив допустимий час");
} else {
console.error("Помилка запиту:", error.message);
}
});finally виконується незалежно від результату операції. У прикладі він очищає таймер після успішного завершення запиту або після помилки.
awaitasync function getUserName() {
const user = fetchJson(
"https://jsonplaceholder.typicode.com/users/1"
);
console.log(user.name); // Помилка: user — це проміс
}Правильно:
async function getUserName() {
const user = await fetchJson(
"https://jsonplaceholder.typicode.com/users/1"
);
console.log(user.name);
}response.okconst response = await fetch("/api/users");
const data = await response.json();Такий код може спробувати обробити помилкову HTTP-відповідь як успішні дані.
Правильно:
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`HTTP-помилка ${response.status}`);
}
const data = await response.json();Технічно можна поєднувати .then() і await, але складний код стає менш зрозумілим:
async function loadData() {
return fetch("/api/data")
.then((response) => response.json())
.then((data) => data.items);
}Якщо вже використовується async, зазвичай зручніше дотримуватися одного стилю:
async function loadData() {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error("Не вдалося отримати дані");
}
const data = await response.json();
return data.items;
}Неоптимальний варіант:
const users = await fetchJson("/api/users");
const posts = await fetchJson("/api/posts");Якщо ці запити не залежать один від одного, краще:
const [users, posts] = await Promise.all([
fetchJson("/api/users"),
fetchJson("/api/posts"),
]);Відхилений проміс без обробника може призвести до необробленої помилки. Для верхнього рівня асинхронної операції додайте try...catch або .catch():
async function main() {
try {
const data = await fetchJson("/api/data");
console.log(data);
} catch (error) {
console.error(error.message);
}
}
main();Асинхронні операції не блокують виконання всього JavaScript-коду.
Promise представляє майбутній результат асинхронної операції.
.then() обробляє успішний результат, а .catch() — помилку.
fetch() повертає проміс із об’єктом Response.
response.json() також є асинхронною операцією та повертає проміс.
fetch() потрібно доповнювати перевіркою response.ok.
async/await робить послідовний асинхронний код зрозумілішим.
Послідовні запити використовують, коли наступний запит залежить від попереднього.
Незалежні запити варто запускати через Promise.all().
Promise.allSettled() дає змогу отримати результати всіх незалежних операцій, навіть якщо деякі завершилися помилкою.
try...catch, finally і AbortController допомагають надійно керувати помилками та часом очікування.