Пошук уроків, статей та іншого контенту
Освоїте синтаксис async і await, перетворите ланцюжки Promise на читабельний код та правильно оброблятимете винятки.
async і awaitasync та await — це синтаксис JavaScript для зручної роботи з асинхронними операціями. Вони використовують Promise, але дають змогу записувати асинхронний код у стилі, схожому на синхронний.
Функція, оголошена з ключовим словом async, завжди повертає Promise.
async function getNumber() {
return 42;
}
getNumber().then((value) => {
console.log(value); // 42
});Навіть якщо функція повертає звичайне значення, JavaScript автоматично обгортає його в успішно виконаний Promise.
Еквівалентний запис без async:
function getNumber() {
return Promise.resolve(42);
}Асинхронною може бути і стрілкова функція:
const getMessage = async () => {
return "Привіт!";
};awaitОператор await призупиняє виконання поточної async-функції, доки Promise не буде виконано або відхилено.
async function loadData() {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const todo = await response.json();
console.log(todo);
}
loadData();Важливо: await не блокує весь JavaScript і не зупиняє браузер. Він призупиняє лише виконання поточної асинхронної функції. Інший код може виконуватися далі.
await повертає значення, з яким завершився Promise:
async function calculate() {
const result = await Promise.resolve(2 + 3);
console.log(result); // 5
}
calculate();Якщо Promise відхиляється, await створює виняток. Його можна обробити за допомогою try...catch.
PromiseРозглянемо код із методами .then() і .catch():
function loadUser() {
return fetch("/api/user")
.then((response) => response.json())
.then((user) => {
console.log(user);
return user;
})
.catch((error) => {
console.error("Не вдалося завантажити користувача", error);
});
}За допомогою async і await той самий код можна записати зрозуміліше:
async function loadUser() {
try {
const response = await fetch("/api/user");
const user = await response.json();
console.log(user);
return user;
} catch (error) {
console.error("Не вдалося завантажити користувача", error);
}
}Кожен виклик await читається як окремий крок:
дочекатися відповіді сервера;
перетворити відповідь у JSON;
використати отримані дані.
try...catchПомилка асинхронної операції перехоплюється так само, як і звичайний виняток:
async function loadTodo() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const todo = await response.json();
console.log("Завдання:", todo.title);
} catch (error) {
console.error("Сталася помилка:", error.message);
}
}
loadTodo();response.okfetch() не відхиляє Promise автоматично для HTTP-помилок, наприклад 404 або 500. У такому випадку fetch() успішно повертає об’єкт Response.
Тому статус відповіді потрібно перевіряти самостійно:
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}Помилки мережі, наприклад відсутність з’єднання, зазвичай призводять до відхилення Promise автоматично.
finallyКод у finally виконується незалежно від результату операції:
async function loadProfile() {
showLoader();
try {
const response = await fetch("/api/profile");
if (!response.ok) {
throw new Error("Не вдалося отримати профіль");
}
return await response.json();
} catch (error) {
console.error(error);
} finally {
hideLoader();
}
}finally зручно використовувати для:
приховування індикатора завантаження;
закриття модального вікна;
звільнення ресурсів;
скидання стану програми.
async-функціїРезультат return в async-функції стає значенням успішного Promise:
async function getUserName() {
return "Олена";
}
async function showUserName() {
const name = await getUserName();
console.log(name); // Олена
}
showUserName();Якщо в async-функції виникає необроблена помилка, її Promise відхиляється:
async function fail() {
throw new Error("Щось пішло не так");
}
fail().catch((error) => {
console.error(error.message);
});Зовнішній код також може обробити помилку через try...catch:
async function main() {
try {
await fail();
} catch (error) {
console.error("Помилка перехоплена:", error.message);
}
}
main();Якщо друга операція залежить від результату першої, її потрібно запускати після першої:
async function loadUserPosts(userId) {
const userResponse = await fetch(`/api/users/${userId}`);
if (!userResponse.ok) {
throw new Error("Користувача не знайдено");
}
const user = await userResponse.json();
const postsResponse = await fetch(`/api/users/${user.id}/posts`);
if (!postsResponse.ok) {
throw new Error("Не вдалося завантажити дописи");
}
const posts = await postsResponse.json();
return {
user,
posts,
};
}Тут другий запит використовує user.id, тому він не може початися раніше за перший.
Послідовне виконання також потрібне, коли операції змінюють спільний стан:
async function saveSettings() {
await saveTheme();
await saveLanguage();
}Якщо асинхронні операції незалежні одна від одної, не потрібно чекати на кожну окремо:
async function loadPageData() {
const userPromise = fetch("/api/user");
const notificationsPromise = fetch("/api/notifications");
const settingsPromise = fetch("/api/settings");
const [userResponse, notificationsResponse, settingsResponse] =
await Promise.all([
userPromise,
notificationsPromise,
settingsPromise,
]);
const [user, notifications, settings] = await Promise.all([
userResponse.json(),
notificationsResponse.json(),
settingsResponse.json(),
]);
return {
user,
notifications,
settings,
};
}Усі три запити починаються майже одночасно. Це швидше, ніж послідовний варіант:
// Повільніший варіант: кожен запит починається після попереднього
const user = await loadUser();
const notifications = await loadNotifications();
const settings = await loadSettings();Promise.allPromise.all() успішно завершується, коли завершилися всі передані Promise.
const results = await Promise.all([
fetch("/api/first"),
fetch("/api/second"),
]);Якщо хоча б один Promise відхилиться, Promise.all() також відхилиться.
Цей метод варто використовувати, коли всі результати потрібні для подальшої роботи.
Promise.allSettledPromise.allSettled() очікує завершення всіх операцій, навіть якщо деякі з них завершилися помилкою:
async function loadWidgets() {
const results = await Promise.allSettled([
loadWeather(),
loadNews(),
loadCurrencyRates(),
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log("Отримано:", result.value);
} else {
console.error("Помилка:", result.reason);
}
}
}Це корисно, коли помилка одного незалежного запиту не повинна скасувати обробку інших.
await на верхньому рівніУ сучасних JavaScript-модулях можна використовувати await поза функцією:
const response = await fetch("/api/config");
const config = await response.json();
console.log(config);Такий код працює в модулях, наприклад у файлі, підключеному з атрибутом type="module":
<script type="module" src="app.js"></script>У звичайному скрипті await має бути всередині async-функції.
Якщо сумісність середовища невідома, можна використати асинхронну функцію:
async function main() {
const response = await fetch("/api/config");
const config = await response.json();
console.log(config);
}
main();await і звичайні значенняawait можна застосовувати не лише до Promise. Якщо передати звичайне значення, JavaScript обгорне його в успішний Promise:
async function example() {
const value = await 10;
console.log(value); // 10
}
example();Проте зазвичай await використовують саме з асинхронними операціями.
async і await не скасовують операції автоматично. Для скасування запиту fetch можна використати AbortController:
async function loadData() {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, 3000);
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1",
{
signal: controller.signal,
}
);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
console.error("Запит скасовано через перевищення часу очікування");
} else {
console.error("Помилка запиту:", error.message);
}
} finally {
clearTimeout(timeoutId);
}
}
loadData();Контролер передається в fetch через властивість signal. Після виклику abort() запит завершується помилкою AbortError.
awaitasync function getUser() {
return fetch("/api/user").then((response) => response.json());
}
async function showUser() {
const user = getUser();
console.log(user.name); // Помилка: user — це Promise
}Потрібно дочекатися результату:
async function showUser() {
const user = await getUser();
console.log(user.name);
}await поза async-функцієюУ звичайному JavaScript-скрипті цей код некоректний:
function loadData() {
const response = await fetch("/api/data");
}Правильний варіант:
async function loadData() {
const response = await fetch("/api/data");
return response;
}Невдалий варіант:
async function loadData() {
const users = await loadUsers();
const products = await loadProducts();
return { users, products };
}Якщо операції незалежні, краще виконати їх паралельно:
async function loadData() {
const [users, products] = await Promise.all([
loadUsers(),
loadProducts(),
]);
return { users, products };
}Якщо викликати async-функцію без await, .catch() або зовнішнього обробника, можна отримати необроблений відхилений Promise:
loadUser();Краще зробити так:
loadUser().catch((error) => {
console.error("Необроблена помилка:", error);
});Або викликати функцію з іншої async-функції:
async function main() {
try {
await loadUser();
} catch (error) {
console.error("Не вдалося виконати операцію:", error);
}
}
main();fetch без перевірки HTTP-статусуНевдалий варіант:
const response = await fetch("/api/data");
const data = await response.json();Краще перевірити response.ok перед читанням тіла відповіді:
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`Сервер повернув статус ${response.status}`);
}
const data = await response.json();Нижче наведено повний приклад функції, яка завантажує список завдань, перевіряє відповідь і повертає лише невиконані завдання:
async function getIncompleteTodos() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos"
);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const todos = await response.json();
return todos.filter((todo) => !todo.completed);
} catch (error) {
console.error("Не вдалося завантажити завдання:", error.message);
return [];
}
}
async function main() {
const todos = await getIncompleteTodos();
console.log(`Незавершених завдань: ${todos.length}`);
for (const todo of todos.slice(0, 3)) {
console.log(`- ${todo.title}`);
}
}
main().catch((error) => {
console.error("Критична помилка програми:", error);
});У цьому прикладі:
getIncompleteTodos є async-функцією;
await fetch(...) очікує відповідь сервера;
response.ok перевіряє HTTP-статус;
try...catch обробляє помилки;
функція повертає звичайний масив через Promise;
main очікує результат за допомогою await.
async перетворює функцію на таку, що повертає Promise.
await очікує завершення Promise всередині async-функції.
Помилки асинхронних операцій обробляють через try...catch.
fetch потрібно перевіряти через response.ok, оскільки HTTP-помилки не завжди відхиляють Promise.
Послідовний await використовуйте для залежних операцій.
Незалежні операції запускайте паралельно за допомогою Promise.all.
Promise.allSettled дає змогу отримати результат кожної операції, навіть якщо деякі завершилися помилкою.
Відхилення async-функції потрібно обробити через try...catch або .catch().
async і await покращують читабельність, але не змінюють асинхронну природу JavaScript.