Пошук уроків, статей та іншого контенту
Організуєте послідовні й паралельні запити, використовуючи async/await, Promise.all() та Promise.allSettled().
У браузері дані сторінки часто надходять із кількох HTTP-джерел:
спочатку потрібно завантажити профіль користувача;
потім — його публікації та завдання;
окремі віджети сторінки можна завантажувати незалежно;
помилка одного необов’язкового запиту не повинна блокувати всю сторінку.
async/await робить асинхронний код схожим на послідовний, а Promise.all() і Promise.allSettled() допомагають керувати кількома промісами.
Метод fetch() повертає проміс. Важливо враховувати, що fetch() відхиляє проміс переважно через мережеву помилку. HTTP-відповіді зі статусами 404 або 500 самі по собі не вважаються помилкою для fetch().
Тому зазвичай перевіряють response.ok:
async function requestJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
return response.json();
}Якщо сервер повернув успішну відповідь, response.json() також повертає проміс, тому його потрібно очікувати за допомогою await.
Послідовний сценарій потрібен, коли наступний запит залежить від результату попереднього.
Наприклад:
завантажити користувача;
отримати його ідентифікатор;
завантажити публікації цього користувача.
async function loadUserPosts(userId) {
const user = await requestJson(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const posts = await requestJson(
`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`
);
return {
user,
posts
};
}
loadUserPosts(1)
.then(({ user, posts }) => {
console.log(`Користувач: ${user.name}`);
console.log(`Кількість публікацій: ${posts.length}`);
})
.catch((error) => {
console.error("Не вдалося завантажити дані:", error);
});У цьому прикладі другий запит не може початися раніше, ніж буде отримано user.id.
Важливо не плутати залежні та незалежні запити. Якщо другому запиту не потрібен результат першого, послідовне очікування лише збільшує час завантаження.
Promise.all()Якщо кілька запитів не залежать один від одного, їх можна запустити одночасно:
async function loadDashboard() {
const [posts, todos, albums] = await Promise.all([
requestJson("https://jsonplaceholder.typicode.com/posts?userId=1"),
requestJson("https://jsonplaceholder.typicode.com/todos?userId=1"),
requestJson("https://jsonplaceholder.typicode.com/albums?userId=1")
]);
return {
posts,
todos,
albums
};
}
loadDashboard()
.then((data) => {
console.log("Публікації:", data.posts.length);
console.log("Завдання:", data.todos.length);
console.log("Альбоми:", data.albums.length);
})
.catch((error) => {
console.error("Не вдалося завантажити панель:", error);
});Усі три виклики requestJson() починають виконуватися до await. Масив, переданий у Promise.all(), містить уже створені проміси.
Promise.all():
повертає проміс;
завершується успішно, коли успішно завершилися всі проміси;
повертає результати в тому самому порядку, що й вхідний масив;
відхиляється, щойно один із промісів відхилено.
Порядок завершення запитів може бути різним, але порядок результатів залишається стабільним:
const [first, second] = await Promise.all([
requestJson("/api/first"),
requestJson("/api/second")
]);first завжди відповідає першому промісу, а second — другому.
На практиці часто потрібна змішана схема:
отримати профіль;
використати його id для наступних запитів;
завантажити публікації та завдання паралельно.
async function loadUserPage(userId) {
const user = await requestJson(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const [posts, todos] = await Promise.all([
requestJson(
`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`
),
requestJson(
`https://jsonplaceholder.typicode.com/todos?userId=${user.id}`
)
]);
return {
user,
posts,
todos
};
}
async function renderUserPage() {
try {
const { user, posts, todos } = await loadUserPage(1);
document.body.innerHTML = `
<h1>${user.name}</h1>
<p>${user.email}</p>
<p>Публікацій: ${posts.length}</p>
<p>Завдань: ${todos.length}</p>
`;
} catch (error) {
document.body.textContent = "Не вдалося завантажити сторінку.";
console.error(error);
}
}
renderUserPage();Перший запит тут є залежністю для двох наступних. Після отримання користувача два незалежні запити виконуються паралельно.
Promise.all()Розглянемо ситуацію, коли один із запитів завершується помилкою:
async function loadRequiredData() {
const [user, posts] = await Promise.all([
requestJson("https://jsonplaceholder.typicode.com/users/1"),
requestJson("https://jsonplaceholder.typicode.com/invalid-resource")
]);
return { user, posts };
}
loadRequiredData()
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error("Не вдалося завантажити всі обов'язкові дані:", error);
});У цьому випадку Promise.all() відхилиться через помилку другого запиту. Така поведінка правильна, якщо без усіх результатів неможливо побудувати сторінку.
Однак інколи дані є незалежними. Наприклад, помилка завантаження рекомендацій не повинна приховувати вже завантажений профіль.
Для такого сценарію краще використовувати Promise.allSettled().
Promise.allSettled()Promise.allSettled() очікує завершення всіх промісів — і успішних, і відхилених:
const results = await Promise.allSettled([
requestJson("https://jsonplaceholder.typicode.com/users/1"),
requestJson("https://jsonplaceholder.typicode.com/invalid-resource"),
requestJson("https://jsonplaceholder.typicode.com/posts?userId=1")
]);
console.log(results);Кожен елемент результату має поле status.
Успішний результат має вигляд:
{
status: "fulfilled",
value: /* результат промісу */
}Відхилений результат має вигляд:
{
status: "rejected",
reason: /* причина помилки */
}Один зі способів обробити результати:
async function loadOptionalWidgets() {
const results = await Promise.allSettled([
requestJson("https://jsonplaceholder.typicode.com/users/1"),
requestJson("https://jsonplaceholder.typicode.com/invalid-resource"),
requestJson("https://jsonplaceholder.typicode.com/posts?userId=1")
]);
const [userResult, recommendationsResult, postsResult] = results;
if (userResult.status === "fulfilled") {
console.log("Профіль:", userResult.value);
} else {
console.error("Профіль недоступний:", userResult.reason);
}
if (recommendationsResult.status === "fulfilled") {
console.log("Рекомендації:", recommendationsResult.value);
} else {
console.warn("Рекомендації недоступні");
}
if (postsResult.status === "fulfilled") {
console.log("Публікації:", postsResult.value);
} else {
console.warn("Публікації недоступні");
}
}
loadOptionalWidgets();На відміну від Promise.all(), Promise.allSettled() не переривається після першої помилки.
У наступному прикладі:
профіль є обов’язковим;
публікації та завдання завантажуються паралельно;
статистика є необов’язковою;
помилка статистики не блокує основний вміст.
async function requestJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP-помилка ${response.status}: ${url}`);
}
return response.json();
}
async function loadProfilePage(userId) {
// Профіль потрібен для наступних запитів
const user = await requestJson(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
// Публікації та завдання незалежні, тому завантажуємо їх паралельно
const [posts, todos] = await Promise.all([
requestJson(
`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`
),
requestJson(
`https://jsonplaceholder.typicode.com/todos?userId=${user.id}`
)
]);
// Додаткові дані не повинні блокувати основну сторінку
const optionalResults = await Promise.allSettled([
requestJson(
`https://jsonplaceholder.typicode.com/albums?userId=${user.id}`
),
requestJson(
"https://jsonplaceholder.typicode.com/invalid-statistics"
)
]);
const [albumsResult, statisticsResult] = optionalResults;
return {
user,
posts,
todos,
albums: albumsResult.status === "fulfilled"
? albumsResult.value
: [],
statistics: statisticsResult.status === "fulfilled"
? statisticsResult.value
: null
};
}
async function showProfilePage() {
try {
const data = await loadProfilePage(1);
const statisticsMessage = data.statistics
? "Статистику завантажено"
: "Статистика тимчасово недоступна";
document.body.innerHTML = `
<h1>${data.user.name}</h1>
<p>Email: ${data.user.email}</p>
<p>Публікацій: ${data.posts.length}</p>
<p>Завдань: ${data.todos.length}</p>
<p>Альбомів: ${data.albums.length}</p>
<p>${statisticsMessage}</p>
`;
} catch (error) {
// Тут обробляємо помилки обов'язкових даних
document.body.textContent = "Не вдалося завантажити профіль.";
console.error(error);
}
}
showProfilePage();Цей код можна виконати в консолі браузера на сторінці, де дозволено виконання JavaScript. Він використовує тестовий HTTP-сервіс, тому результат залежить від доступності мережі.
awaitВикористовуйте, коли:
наступний запит потребує результату попереднього;
потрібно виконувати операції в чіткій черзі;
результат першого запиту визначає, чи потрібно виконувати наступний.
const account = await loadAccount();
const permissions = await loadPermissions(account.id);Promise.all()Використовуйте, коли:
усі запити є обов’язковими;
запити незалежні;
помилка будь-якого запиту повинна вважатися помилкою всієї операції.
const [profile, settings] = await Promise.all([
loadProfile(),
loadSettings()
]);Promise.allSettled()Використовуйте, коли:
запити незалежні;
потрібно отримати результат кожного запиту;
частковий успіх є прийнятним;
помилка одного віджета не повинна скасувати інші.
const results = await Promise.allSettled([
loadNews(),
loadRecommendations(),
loadWeather()
]);Не варто писати так:
const users = await requestJson("/api/users");
const posts = await requestJson("/api/posts");
const comments = await requestJson("/api/comments");Якщо запити незалежні, вони виконуватимуться один за одним.
Краще:
const [users, posts, comments] = await Promise.all([
requestJson("/api/users"),
requestJson("/api/posts"),
requestJson("/api/comments")
]);Promise.all() після awaitТакий код уже не створює паралельне виконання:
const users = await requestJson("/api/users");
const posts = await requestJson("/api/posts");
const result = await Promise.all([users, posts]);У Promise.all() передаються вже готові значення, а самі запити раніше виконувалися послідовно.
response.okfetch() не відхиляє проміс автоматично для статусів 404 або 500.
Проблемний варіант:
async function requestJson(url) {
const response = await fetch(url);
return response.json();
}Якщо сервер повернув HTML-сторінку помилки або інший формат, помилка може виникнути лише під час розбору відповіді. Перевіряйте response.ok до виклику response.json().
Promise.allSettled() без перевірки статусуНе можна безпечно звертатися до result.value, не перевіривши result.status:
const [result] = await Promise.allSettled([
requestJson("/api/data")
]);
if (result.status === "fulfilled") {
console.log(result.value);
} else {
console.error(result.reason);
}У відхиленого результату немає корисного value, а в успішного результату немає reason.
Асинхронна функція може повернути відхилений проміс. Для верхнього рівня застосунку використовуйте try...catch:
async function start() {
try {
const data = await loadData();
console.log(data);
} catch (error) {
console.error("Помилка завантаження:", error);
}
}
start();Якщо Promise.all() відхилився через один запит, інші запити не обов’язково припиняються. Вони можуть продовжити виконання у фоновому режимі.
Якщо запити потрібно скасувати, використовуйте AbortController і передавайте його сигнал у fetch():
const controller = new AbortController();
fetch("/api/data", {
signal: controller.signal
});
// Скасування запиту
controller.abort();async/await зручний для читання асинхронного коду.
Послідовний await потрібен для залежних операцій.
Незалежні запити варто запускати паралельно.
Promise.all() успішний лише тоді, коли успішні всі проміси.
Promise.allSettled() повертає результат кожного промісу, навіть якщо частина завершилася помилкою.
fetch() не вважає HTTP-статуси 4xx і 5xx винятками, тому перевіряйте response.ok.
Оптимальна композиція часто поєднує послідовний запит для отримання залежностей і паралельні запити для незалежних даних.