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
Chrome Edge Firefox Internet Explorer Opera Safari WebView Android Chrome Android Firefox for Android Opera Android Safari on iOS Samsung Internet Deno Node.js
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
footnote

Дивіться також