await
Оператор await
(очікувати, дожидатися) застосовується для очікування об'єкта Promise
і отримання його значення сповнення. Він може вживатися лише всередині асинхронної функції та на зовнішньому рівні модулів.
Синтаксис
await expression
Параметри
expression
Об'єкт
Promise
, очікуваний об'єкт чи іще якесь значення, на котре можна почекати.
Повернене значення
Значення сповнення промісу або очікуваного об'єкта, або, якщо вираз не є очікуваним, то саме значення такого виразу.
Винятки
Викидає причину відхилення, якщо проміс або очікуваний об'єкт – відхиляється.
Опис
await
зазвичай вживається для розгортання промісів шляхом передачі Promise
як expression
. Використання await
призупиняє виконання async
функції навколо, поки проміс не залагоджується (тобто сповнюється або відхиляється). Коли виконання відновлюється, то значенням виразу await
стає значення сповненого промісу.
Якщо проміс відхиляється, то вираз await
викидає значення відхилення. Функція, котра містить цей вираз await
, з'явиться у трасуванні стека помилки. Інакше, якщо на відхилений проміс не очікують, або якщо він зразу повертається, то функція-викликач не з'явиться у трасуванні стека.
expression
вирішується так само, як Promise.resolve()
: він завжди перетворюється на нативний Promise
, а тоді на нього очікують. Якщо expression
є:
- Нативним
Promise
(тобто якщоexpression
належить класовіPromise
або його підкласові, іexpression.constructor === Promise
): то проміс використовується безпосередньо, і на нього нативно очікують, без викликуthen()
. - Очікуваним об'єктом (включно з ненативними промісами, породженнями поліфілів, заступниками, примірниками дочірніх класів тощо): то новий проміс утворюється конструктором
Promise()
шляхом виклику методуthen()
цього об'єкта і передачі в нього обробника, що викликає функцію зворотного викликуresolve
. - Чимось іншим: утворюється та використовується зразу сповнений
Promise
.
Навіть коли вжитий проміс є зразу сповненим, то виконання асинхронної функції все одно призупиняється до наступного такту. Тим часом відновлює виконання викликач цієї асинхронної функції. Дивіться приклад нижче.
Через те, що await
є дійсним лише всередині асинхронних функцій та модулів, котрі самі є асинхронними та повертають проміси, вираз await
ніколи не блокує головний потік і відкладає виконання лише того коду, котрий насправді залежить від результату, тобто всього, що стоїть після виразу await
.
Приклади
Очікування на сповнення промісу
Якщо у вираз await
переданий Promise
, то цей вираз очікує на сповнення Promise
і повертає значення сповнення.
function resolveAfter2Seconds(x) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function f1() {
const x = await resolveAfter2Seconds(10);
console.log(x); // 10
}
f1();
Очікувані об'єкти
Очікувані об'єкти вирішуються так само, як справжні об'єкти Promise
.
async function f() {
const thenable = {
then(resolve, _reject) {
resolve("вирішено!");
},
};
console.log(await thenable); // "вирішено!"
}
f();
Вони так само можуть бути відхилені:
async function f() {
const thenable = {
then(resolve, reject) {
reject(new Error("відхилено!"));
},
};
await thenable; // Викидає Error: відхилено!
}
f();
Перетворення на проміс
Якщо значення не є Promise
, то await
перетворює його на вирішений Promise
, на котрий очікує. Ідентичність очікуваного значення не змінюється, за умови що воно не має властивості then
, котру можна викликати як метод.
async function f3() {
const y = await 20;
console.log(y); // 20
const obj = {};
console.log((await obj) === obj); // true
}
f3();
Обробка відхилених промісів
Якщо Promise
відхиляється, то значення його відхилення викидається як помилка.
async function f4() {
try {
const z = await Promise.reject(30);
} catch (e) {
console.error(e); // 30
}
}
f4();
Відхилені проміси можна обробляти без блоку try
– шляхом додавання в ланцюжок перед очікуванням на проміс обробника catch()
.
const response = await promisedFunction().catch((err) => {
console.error(err);
return "усталена відповідь";
});
// response буде "усталена відповідь", якщо проміс відхилиться
Це за умови, що promisedFunction()
ніколи не викидає синхронної помилки, а щоразу повертає відхилений проміс. Так це працює в більшості як слід спроєктованих функцій на основі промісів, котрі зазвичай мають такий вигляд:
function promisedFunction() {
// Негайно повернути проміс – для мінімізації шансів того, що буде викинута помилка
return new Promise((resolve, reject) => {
// якісь асинхронні дії
});
}
Проте якщо promisedFunction()
викине помилку синхронно, то така помилка не буде перехоплена обробником catch()
. Для таких випадків необхідна інструкція try...catch
.
await на зовнішньому рівні
Ключове слово await
можна застосовувати само по собі (поза асинхронною функцією) на зовнішньому рівні модуля. Це означає, що модулі з дочірніми модулями, котрі використовують await
, очікуватимуть виконання дочірніх модулів, поки зможуть виконатись самі, хоч це й не блокує завантаження інших дочірніх модулів.
Ось приклад простого модуля, що використовує API Fetch і задає await
перед інструкцією export
. Усі модулі, що включать цей модуль, очікуватимуть вирішення отримання, перш ніж виконати будь-який код
// запит на отримання
const colors = fetch("../data/colors.json").then((response) => response.json());
export default await colors;
Вплив await на плин виконання
Коли в коді зустрічається await
(як в асинхронній функції, так і в модулі), то очікуваний вираз виконується, а ввесь код, що очікує на значення виразу, призупиняється та додається в кінець черги мікрозадач. Потім головний потік звільняється для наступної задачі в циклі подій. Це відбувається навіть тоді, коли очікуване значення є зразу вирішеним промісом або не промісом узагалі. Для прикладу – наступний код:
async function foo(name) {
console.log(name, "початок");
console.log(name, "середина");
console.log(name, "кінець");
}
foo("Перший");
foo("Другий");
// Перший початок
// Перший середина
// Перший кінець
// Другий початок
// Другий середина
// Другий кінець
У цьому випадку дві асинхронні функції є фактично синхронними, адже не містять жодних виразів await
. Усі три інструкції виконуються за один такт. Висловлюючись поняттями промісів, така функція відповідає наступному кодові:
function foo(name) {
return new Promise((resolve) => {
console.log(name, "початок");
console.log(name, "середина");
console.log(name, "кінець");
resolve();
});
}
Проте коли з'являється принаймні один await
, то функція стає асинхронною, а виконання подальших інструкцій відкладається до наступного такту.
async function foo(name) {
console.log(name, "початок");
await console.log(name, "середина");
console.log(name, "кінець");
}
foo("Перший");
foo("Другий");
// Перший початок
// Перший середина
// Другий початок
// Другий середина
// Перший кінець
// Другий кінець
Це відповідає наступному кодові:
function foo(name) {
return new Promise((resolve) => {
console.log(name, "початок");
resolve(console.log(name, "середина"));
}).then(() => {
console.log(name, "кінець");
});
}
Хоч додатковий then()
не є необхідністю – він може бути поєднаний з функцією-виконавцем, переданою в конструктор, та існування обробника then()
означає, що код витратить на своє завершення на один такт більше. Те саме відбувається з await
. Таким чином, слід слідкувати, аби await
вживався лише тоді, коли це необхідно (для розгортання промісів до інших значень).
Інші мікрозадачі можуть виконуватись до того, як відновить виконання призупинена асинхронна функція. Приклад нижче застосовує queueMicrotask()
, аби продемонструвати те, як обробляється черга мікрозадач, коли зустрічається кожний вираз await
.
let i = 0;
queueMicrotask(function test() {
i++;
console.log("мікрозадача", i);
if (i < 3) {
queueMicrotask(test);
}
});
(async () => {
console.log("асинхронна функція починається");
for (let i = 1; i < 3; i++) {
await null;
console.log("асинхронна функція прокидається", i);
}
await null;
console.log("асинхронна функція завершується");
})();
queueMicrotask(() => {
console.log("queueMicrotask() після виклику асинхронної функції");
});
console.log("завершення синхронної частини сценарію");
// Виводить:
// асинхронна функція починається
// завершення синхронної частини сценарію
// мікрозадача 1
// асинхронна функція прокидається 1
// queueMicrotask() після виклику асинхронної функції
// мікрозадача 2
// асинхронна функція прокидається 2
// мікрозадача 3
// асинхронна функція завершується
У цьому прикладі функція test()
завжди викликається до відновлення виконання асинхронної функції, тож заплановані мікрозадачі завжди виконуються в закручений спосіб. З іншого боку, через те, що і await
і queueMicrotask()
планують мікрозадачі, то порядок виконання завжди заснований на порядку планування. Саме тому вивід "queueMicrotask() після виклику асинхронної функції" відбувається після першого відновлення виконання асинхронної функції.
Покращення трасування стека
Іноді await
опускають, коли з асинхронної функції безпосередньо повертають проміс.
async function noAwait() {
// Якісь дії...
return /* await */ lastAsyncTask();
}
Проте слід врахувати випадок, коли lastAsyncTask
асинхронно викидає помилку.
async function lastAsyncTask() {
await null;
throw new Error("не вийшло");
}
async function noAwait() {
return lastAsyncTask();
}
noAwait();
// Error: не вийшло
// at lastAsyncTask
Лише lastAsyncTask
з'являється в трасуванні стека, тому що проміс відхиляється після того, як він був повернений з noAwait
— у певному розумінні він не пов'язаний з noAwait
. Аби покращити трасування стека, можна застосувати await
для розгортання промісу, аби виняток викидався з поточної функції. Цей виняток буде негайно загорнуто в новий відхилений проміс, але під час створення помилки функція-викликач буде додана в трасування стека.
async function lastAsyncTask() {
await null;
throw new Error("не вийшло");
}
async function withAwait() {
return await lastAsyncTask();
}
withAwait();
// Error: не вийшло
// at lastAsyncTask
// at async withAwait
Усупереч поширеному уявленню, return await promise
– щонайменш так само швидка операція, як return promise
, що пов'язано з тим, як специфікація та рушії оптимізують розв'язання нативних промісів. Існує пропозиція зробити return promise
швидшою, а також можна прочитати про оптимізацію V8 щодо асинхронних функцій. Отже, за винятком стилістичних міркувань, майже завжди краще використовувати return await
.
Специфікації
Сумісність із браузерами
desktop | mobile | server | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
await
|
Chrome Full support 55 | Edge Full support 14 | Firefox Full support 52 | Internet Explorer No support Ні | Opera Full support 42 | Safari Full support 10.1 | WebView Android Full support 55 | Chrome Android Full support 55 | Firefox for Android Full support 52 | Opera Android Full support 42 | Safari on iOS Full support 10.3 | Samsung Internet Full support 6.0 | Deno Full support 1.0 | Node.js Full support 7.6.0 |
Use at module top level | Chrome Full support 89 | Edge Full support 89 | Firefox Full support 89 | Internet Explorer No support Ні | Opera Full support 75 | Safari Full support 15 | WebView Android Full support 89 | Chrome Android Full support 89 | Firefox for Android Full support 89 | Opera Android Full support 63 | Safari on iOS Full support 15 | Samsung Internet Full support 15.0 | Deno Full support 1.0 | Node.js Full support 14.8.0 |