Пошук уроків, статей та іншого контенту
Застосуємо async/await для послідовного читабельного коду та зрозуміємо його зв’язок із Promise.
async/awaitasync/await — синтаксис для роботи з асинхронним кодом у JavaScript. Він дає змогу записувати операції з Promise у послідовному стилі, схожому на звичайний синхронний код.
Розглянемо Promise-ланцюжок:
getUser()
.then((user) => getOrders(user.id))
.then((orders) => {
console.log(orders);
})
.catch((error) => {
console.error(error);
});Той самий код із async/await:
async function showOrders() {
try {
const user = await getUser();
const orders = await getOrders(user.id);
console.log(orders);
} catch (error) {
console.error(error);
}
}async/await не замінює Promise. Це зручніший синтаксис для роботи з ними.
asyncКлючове слово async перед функцією означає, що функція завжди повертає Promise.
async function getMessage() {
return "Привіт";
}
getMessage().then((message) => {
console.log(message);
});Хоча функція повертає звичайний рядок, фактичний результат буде обгорнутий у виконаний Promise.
Ці два варіанти еквівалентні:
async function getNumber() {
return 42;
}function getNumber() {
return Promise.resolve(42);
}async можна використовувати також зі стрілковими функціями:
const getNumber = async () => {
return 42;
};awaitawait очікує завершення Promise і повертає його результат.
async function loadData() {
const result = await Promise.resolve("Дані отримано");
console.log(result);
}
loadData();У цьому прикладі:
Promise.resolve("Дані отримано") створює виконаний Promise.
await отримує значення з Promise.
Рядок "Дані отримано" записується у змінну result.
await можна використовувати всередині async-функції:
async function example() {
const value = await somePromise();
}Якщо Promise ще не завершився, виконання поточної async-функції призупиняється до його завершення. При цьому Node.js не блокує весь процес: інші асинхронні операції можуть виконуватися далі.
await особливо зручний, коли наступна операція залежить від результату попередньої.
function wait(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
async function processOrder() {
console.log("Початок обробки");
await wait(1000);
console.log("Замовлення перевірено");
await wait(1000);
console.log("Оплату підтверджено");
await wait(1000);
console.log("Замовлення відправлено");
}
processOrder();Повідомлення з’являться послідовно з інтервалом приблизно в одну секунду:
Початок обробки
Замовлення перевірено
Оплату підтверджено
Замовлення відправленоУ цьому прикладі кожен наступний крок починається після завершення попереднього.
Результат асинхронної операції можна записати у змінну:
function getUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 7,
name: "Олена",
});
}, 500);
});
}
async function showUser() {
const user = await getUser();
console.log(user.name);
}
showUser();await getUser() повертає об’єкт, переданий у resolve.
Цей код еквівалентний Promise-ланцюжку:
function showUser() {
return getUser().then((user) => {
console.log(user.name);
});
}try...catchЯкщо Promise відхилено через reject або в асинхронній функції виникла помилка, await викидає виняток. Його можна обробити через try...catch.
function loadUser() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Не вдалося завантажити користувача"));
}, 500);
});
}
async function showUser() {
try {
const user = await loadUser();
console.log(user);
} catch (error) {
console.error("Помилка:", error.message);
}
}
showUser();Результат:
Помилка: Не вдалося завантажити користувачаБлок try містить операції, які можуть завершитися помилкою. Блок catch виконується, якщо така помилка сталася.
Можна обробляти помилку на рівні коду, який викликає async-функцію:
async function loadData() {
throw new Error("Помилка завантаження");
}
loadData().catch((error) => {
console.error(error.message);
});Це працює тому, що async-функція повертає відхилений Promise, якщо всередині неї виникає необроблена помилка.
async-функціїРезультат async-функції потрібно отримувати через await або .then().
async function calculateTotal() {
const price = 100;
const quantity = 3;
return price * quantity;
}
async function showTotal() {
const total = await calculateTotal();
console.log(`Сума: ${total}`);
}
showTotal();Не можна отримати значення безпосередньо так:
const total = calculateTotal();
console.log(total); // Promise, а не числоПравильний варіант:
const total = await calculateTotal();або:
calculateTotal().then((total) => {
console.log(total);
});У Node.js модуль node:fs/promises надає Promise-версії операцій із файлами. Їх зручно використовувати з async/await.
const fs = require("node:fs/promises");
async function readConfig() {
try {
const content = await fs.readFile("config.json", "utf8");
const config = JSON.parse(content);
console.log("Назва застосунку:", config.name);
console.log("Порт:", config.port);
} catch (error) {
if (error.code === "ENOENT") {
console.error("Файл config.json не знайдено");
} else if (error instanceof SyntaxError) {
console.error("Файл містить некоректний JSON");
} else {
console.error("Не вдалося прочитати конфігурацію:", error.message);
}
}
}
readConfig();Для запуску створіть поруч із файлом JavaScript файл config.json:
{
"name": "Demo server",
"port": 3000
}Виклик fs.readFile повертає Promise. await очікує завершення читання файлу, а потім передає його вміст у JSON.parse.
Якщо операції залежать одна від одної, їх потрібно виконувати послідовно:
async function createReport() {
const user = await loadUser();
const orders = await loadOrders(user.id);
return { user, orders };
}loadOrders потребує user.id, тому її не можна коректно запустити до завершення loadUser.
Якщо операції незалежні, послідовний await може непотрібно збільшити час виконання:
async function loadPageData() {
const users = await loadUsers();
const products = await loadProducts();
return { users, products };
}Тут завантаження користувачів і товарів не залежить одне від одного. Їх можна запустити паралельно за допомогою Promise.all:
async function loadPageData() {
const [users, products] = await Promise.all([
loadUsers(),
loadProducts(),
]);
return { users, products };
}Promise.all повертає Promise, який виконується після успішного завершення всіх переданих операцій. Якщо хоча б одна операція завершується помилкою, весь Promise.all буде відхилено.
function wait(milliseconds, value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), milliseconds);
});
}
async function sequential() {
const startedAt = Date.now();
const first = await wait(1000, "перша");
const second = await wait(1000, "друга");
console.log(first, second);
console.log(`Послідовно: ${Date.now() - startedAt} мс`);
}
async function parallel() {
const startedAt = Date.now();
const [first, second] = await Promise.all([
wait(1000, "перша"),
wait(1000, "друга"),
]);
console.log(first, second);
console.log(`Паралельно: ${Date.now() - startedAt} мс`);
}
async function main() {
await sequential();
await parallel();
}
main();Приблизний результат:
перша друга
Послідовно: 2000 мс
перша друга
Паралельно: 1000 мсПаралельне виконання не завжди є правильним. Якщо друга операція використовує результат першої, потрібно залишити послідовний await.
awaitasync function showUser() {
const user = getUser();
console.log(user.name);
}user у цьому випадку є Promise, а не готовим об’єктом користувача.
Правильно:
async function showUser() {
const user = await getUser();
console.log(user.name);
}asyncfunction showUser() {
const user = await getUser();
}await не можна використовувати у звичайній функції. Додайте async:
async function showUser() {
const user = await getUser();
}const first = await loadFirst();
const second = await loadSecond();Якщо операції незалежні, краще:
const [first, second] = await Promise.all([
loadFirst(),
loadSecond(),
]);Необроблені помилки асинхронного коду можуть призвести до відхиленого Promise, про який код не повідомить користувачу належним чином.
Для операцій, які можуть завершитися помилкою, використовуйте try...catch:
async function run() {
try {
await performOperation();
} catch (error) {
console.error(error.message);
}
}forEach з awaitforEach не очікує завершення асинхронного callback:
items.forEach(async (item) => {
await processItem(item);
});Якщо операції потрібно виконати послідовно, використовуйте for...of:
for (const item of items) {
await processItem(item);
}Якщо операції незалежні, запустіть їх паралельно:
await Promise.all(
items.map((item) => processItem(item))
);async перетворює функцію на таку, що повертає Promise.
await отримує результат Promise і дозволяє записувати асинхронний код послідовно.
async/await є зручним синтаксисом поверх Promise, а не окремим механізмом асинхронності.
Помилки під час await обробляють через try...catch.
Результат async-функції потрібно отримувати за допомогою await або .then().
Послідовний await підходить для залежних операцій.
Для незалежних операцій використовуйте Promise.all, щоб виконувати їх паралельно.
await потрібно використовувати всередині async-функції, а в циклах — обирати між послідовним for...of і паралельним Promise.all.