Існує спеціальний синтаксис для більш зручної роботи з промісами, який називається “async/await”. Його напрочуд легко зрозуміти та використовувати.
Асинхронні функції
Почнемо з ключового слова async
. Його можна розмістити перед функцією, наприклад:
async function f() {
return 1;
}
Слово async
перед функцією означає одну просту річ: функція завжди повертає проміс. Інші значення автоматично загортаються в успішно виконаний проміс.
Наприклад, ця функція повертає успішно виконаний проміс з результатом 1
; протестуймо:
async function f() {
return 1;
}
f().then(alert); // 1
…Ми могли б явно повернути проміс, результат буде таким самим:
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
Отже, async
гарантує, що функція повертає проміс і обгортає в нього не-проміси. Досить просто, правда? Але це ще не все. Є ще одне ключове слово, await
, яке працює лише всередині async
-функцій, і воно досить круте.
Await
Синтаксис:
// працює лише всередині async-функцій
let value = await promise;
Ключове слово await
змушує JavaScript чекати, поки проміс не виконається, та повертає його результат.
Ось приклад з промісом, який виконується за 1 секунду:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("готово!"), 1000)
});
let result = await promise; // чекатиме, поки проміс не виконається (*)
alert(result); // "готово!"
}
f();
Виконання функції “призупиняється” у рядку (*)
і відновлюється, коли проміс виконається, а результатом стає result
. Отже, код вище показує “готово!” через одну секунду.
Підкреслимо: await
буквально призупиняє виконання функції до тих пір, поки проміс не виконається, а потім відновлює її з результатом проміса. Це не вимагає жодних ресурсів ЦП, тому що рушій JavaScript може тим часом робити інші завдання: виконувати інші скрипти, обробляти події тощо.
Це просто більш елегантний синтаксис отримання результату проміса, ніж promise.then
. Зокрема, так це легше читати й писати.
await
у звичайних функціяхЯкщо ми спробуємо використати await
у не-асинхронній функції, виникне синтаксична помилка:
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
Ми можемо отримати цю помилку, якщо забудемо поставити async
перед функцією. Як було сказано раніше, await
працює лише всередині async
-функцій.
Давайте візьмемо за приклад showAvatar()
з розділу Ланцюжок промісів і перепишемо його за допомогою async/await
:
- Нам потрібно замінити виклики
.then
наawait
. - Також ми повинні оголосити функцію як
async
, щоб вони працювали.
async function showAvatar() {
// зчитуємо наш JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// зчитуємо користувача github
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// показуємо аватар
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// очікуємо 3 секунди
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
Досить зрозуміло та легко читається, правда? Набагато краще, ніж раніше.
await
верхнього рівня в модуляхУ сучасних браузерах await
на верхньому рівні працює чудово, якщо ми знаходимося всередині модуля. Ми розглянемо модулі в статті Вступ до модулів.
Наприклад:
// ми припускаємо, що цей код виконується на верхньому рівні вкладеності всередині модуля
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
console.log(user);
Якщо ми не використовуємо модулі, або повинні підтримувати старіші браузери, існує універсальний рецепт: загорнути код в анонімну асинхронну функцію.
Ось так:
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
await
працює з “thenable”-об’єктамиЯк і promise.then
, await
дозволяє нам використовувати thenable-об’єкти (їх можна викликати методом then
). Ідея полягає в тому, що сторонній об’єкт може не бути промісом, але бути сумісним з промісом: якщо він підтримує .then
, цього достатньо, щоб використовувати його з await
.
Ось приклад класу Thenable
; нижче await
приймає його екземпляри:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// виконається з this.num*2 через 1000мс
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// чекатиме 1 секунду, після чого результат стане 2
let result = await new Thenable(1);
alert(result);
}
f();
Якщо await
отримує об’єкт не-проміс із .then
, він викликає цей метод, що надає як аргументи вбудовані функції resolve
та reject
(так само як це робиться для звичайного виконання Promise
). Потім await
чекає, поки не буде викликано один з них (у вищенаведеному прикладі це відбувається в рядку (*)
), а потім переходить до результату.
Щоб оголосити асинхронний метод класу, просто додайте перед ним async
:
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1 (це те ж саме, що й (result => alert(result)))
Сенс той самий: це гарантує, що повернуте значення буде промісом, і дозволяє використовувати await
.
Обробка помилок
Якщо проміс виконується нормально, то await promise
повертає результат. Але у випадку завершення з помилкою він генерує помилку, ніби в цьому рядку був оператор throw
.
Цей код:
async function f() {
await Promise.reject(new Error("Упс!"));
}
…робить те ж саме, що й цей:
async function f() {
throw new Error("Упс!");
}
У реальних ситуаціях може пройти деякий час, перш ніж проміс завершиться з помилкою. У цьому випадку буде затримка, перш ніж await
видасть помилку.
Ми можемо зловити цю помилку за допомогою try..catch
, так само як і звичайний throw
:
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
У разі помилки керування переходить до блоку catch
. Ми також можемо обгорнути таким чином кілька рядків:
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// перехоплює помилки як у fetch, так і в response.json
alert(err);
}
}
f();
Якщо у нас немає try..catch
, тоді проміс, згенерований викликом асинхронної функції f()
, завершиться з помилкою. Ми можемо додати .catch
для її обробки:
async function f() {
let response = await fetch('http://no-such-url');
}
// f() поверне проміс, що завершився з помилкою
f().catch(alert); // TypeError: failed to fetch // (*)
Якщо ми забудемо додати .catch
, то отримаємо необроблену помилку проміса (можна переглянути в консолі). Ми можемо відловити такі помилки за допомогою глобального обробника події unhandledrejection
, як описано в розділі Проміси: обробка помилок.
async/await
та promise.then/catch
Коли ми використовуємо async/await
, нам рідко потрібен .then
, тому що await
обробляє очікування за нас. І ми можемо використовувати звичайний try..catch
замість .catch
. Зазвичай (але не завжди) це зручніше.
Але на верхньому рівні вкладеності коду, коли ми знаходимося за межами будь-якої функції async
, ми синтаксично не можемо використовувати await
, тому звичайна практика – додати .then/catch
для обробки кінцевого результату або помилки, що була повернута, як у рядку “(*)” у прикладі вище.
async/await
чудово працює з Promise.all
Коли нам потрібно дочекатися кількох промісів, ми можемо загорнути їх у Promise.all
, а потім додати await
:
// чекаємо масив результатів
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
У разі помилки вона передаватиметься як зазвичай: від невдалого проміса до Promise.all
, а потім стає винятком, який ми можемо зловити за допомогою try..catch
навколо виклику.
Підсумки
Ключове слово async
перед функцією має два ефекти:
- Змушує її завжди повертати проміс.
- Дозволяє використовувати в ній
await
.
Ключове слово await
перед промісом змушує JavaScript чекати, поки цей проміс не виконається, а потім:
- Якщо це помилка, генерується виняток — так само, ніби
throw error
було викликано саме в цьому місці. - В іншому випадку він повертає результат.
Разом вони забезпечують чудову структуру для написання асинхронного коду, який легко і читати, і писати.
За допомогою async/await
нам рідко потрібно писати promise.then/catch
, але ми все одно не повинні забувати, що вони засновані на промісах, тому що іноді (наприклад, на верхньому рівні вкладеності) нам доводиться використовувати ці методи. Також Promise.all
стає в нагоді, коли ми чекаємо на виконання багатьох завдань одночасно.