async function

Оголошення async function оголошує асинхронну функцію, всередині тіла якої дозволене вживання ключового слова await. Ключові слова async та await дають змогу записувати асинхронну (засновану на промісах) поведінку в чистішому стилі, уникаючи необхідності явно задавати ланцюжки промісів.

Асинхронні функції також можна оголошувати як вирази.

Спробуйте його в дії

Синтаксис

async function name(param0) {
  statements
}
async function name(param0, param1) {
  statements
}
async function name(param0, param1, /* … ,*/ paramN) {
  statements
}

Параметри

name

Назва функції.

param Необов'язкове

Назва аргументу, який передається до функції.

statements Необов'язкове

Інструкції, що формують тіло функції. Може застосовуватись функціонал await.

Повернене значення

Проміс, який буде вирішено зі значенням, поверненим з асинхронної функції, або відхилено з винятком, викинутим із (або не обробленим всередині) асинхронної функції.

Опис

Асинхронні функції можуть містити нуль чи більше виразів await. Вираз await змушує функції, які повертають проміси, поводити себе так, наче вони синхронні, шляхом призупинення виконання до моменту, коли повернений проміс буде сповнено або відхилено. Значення, повернене з виконаного промісу, обробляється як значення, повернене виразом await. Вживання async та await дає змогу застосовувати звичайні блоки try / catch навколо асинхронного коду.

Примітка: Ключове слово await дійсне лише всередині асинхронної функції в коді на JavaScript. Якщо його вжити за межами тіла асинхронної функції, програма викине SyntaxError.

Самостійний await можна використовувати всередині модулів JavaScript.

Примітка: Мета async/await — спростити синтаксис, необхідний для користування API, заснованим на промісах. Поведінка async/await — схожа до комбінації генераторів із промісами.

Асинхронні функції завжди повертають проміс. Якщо повернене з асинхронної функції значення не є явним промісом, воно буде неявно обгорнуте в нього.

Для прикладу, зверніть увагу на наступний код:

async function foo() {
  return 1;
}

Він є аналогічним до такого фрагмента:

function foo() {
  return Promise.resolve(1);
}

Примітка:

Попри те, що значення, повернене з асинхронної функції поводить себе так, наче воно загорнене у Promise.resolve, вони не еквівалентні.

Асинхронна функція поверне нове посилання, тоді як Promise.resolve повертає те саме посилання, якщо переданим значенням є проміс.

Це може спричинити проблеми, якщо необхідно перевірити рівність промісу і поверненого з асинхронної функції значення.

const p = new Promise((res, rej) => {
  res(1);
});

async function asyncReturn() {
  return p;
}

function basicReturn() {
  return Promise.resolve(p);
}

console.log(p === basicReturn()); // true
console.log(p === asyncReturn()); // false

Можна сприймати тіло асинхронної функції так, як наче воно поділене на частини виразами await. Код верхнього рівня, аж до першого виразу await включно (якщо такий є), — виконується синхронно. В такому разі асинхронна функція без виразу await вся виконається синхронно. Проте якщо всередині тіла функції є принаймні один вираз await, асинхронна функція завжди виконуватиметься асинхронно.

Наприклад:

async function foo() {
  await 1;
}

Такий вираз також еквівалентний щодо наступного фрагмента:

function foo() {
  return Promise.resolve(1).then(() => undefined);
}

Код після кожного виразу await можна сприймати так, як наче він існує всередині функції зворотного виклику .then. Таким чином, ланцюжок промісів поступово формується з кожним повторним входом у функцію. Повернене значення стає кінцевою ланкою ланцюжка.

В наступному прикладі відбувається очікування на виконання двох промісів. Процес виконання функції foo відбувається у три етапи.

  1. Перший рядок тіла функції foo виконується асинхронно, причому вираз await стає зайнятим очікуванням на виконання промісу. Виконання функції foo на цьому зупиняється, і керування передається назад у функцію, яка викликала foo.
  2. Через деякий час, коли перший проміс сповнюється чи відхиляється, керування передається назад у foo. Результат виконання першого промісу (за умови, що проміс не було відхилено) повертається з виразу await. В цьому випадку 1 присвоюється змінній result1. Виконання функції продовжується, і обчислюється другий вираз await. І знову, процес виконання функції foo зупиняється, і керування передається назовні.
  3. Через деякий час, коли вже другий проміс або сповнено, або відхилено, керування повертається до функції foo. Результат виконання другого промісу повертається з другого виразу await. Тут змінній result2 присвоюється значення 2. Керування передається виразу return (якщо такий є). В цьому конкретному випадку повертається усталене значення undefined як вирішене значення промісу.
async function foo() {
  const result1 = await new Promise((resolve) =>
    setTimeout(() => resolve("1"))
  );
  const result2 = await new Promise((resolve) =>
    setTimeout(() => resolve("2"))
  );
}
foo();

Слід зауважити, що ланцюжок промісів будується не одразу. Натомість він конструюється етапами, по ходу успішного повернення керування з асинхронної функції – і передачі його назад до неї. Як наслідок, слід бути обережним з плином обробки помилок під час роботи з одночасними асинхронними операціями.

Наприклад, в наведеному нижче коді викинеться помилка неопрацьованого відхилення промісу, навіть якщо далі по ходу виконання ланцюжка промісів було встановлено обробник .catch. Це відбувається через те, що p2 не буде "підключено до" ланцюжка промісів доти, доки керування не повернеться із p1.

async function foo() {
  const p1 = new Promise((resolve) => setTimeout(() => resolve("1"), 1000));
  const p2 = new Promise((_, reject) => setTimeout(() => reject("2"), 500));
  const results = [await p1, await p2]; // Так робити не слід! Натомість краще вжити Promise.all чи Promise.allSettled.
}
foo().catch(() => {}); // Спроба проковтнути всі помилки...

Оголошення async function піднімаються нагору своєї області видимості й можуть викликатися на всьому її протязі.

Приклади

Асинхронні функції та порядок виконання

function resolveAfter2Seconds() {
  console.log("початок повільного промісу");
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("повільно");
      console.log("повільний проміс виконано");
    }, 2000);
  });
}

function resolveAfter1Second() {
  console.log("початок швидкого промісу");
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("шкидко");
      console.log("швидкий проміс виконано");
    }, 1000);
  });
}

async function sequentialStart() {
  console.log("==ПОСЛІДОВНИЙ ПОЧАТОК==");

  // 1. Виконання підходить до цього місця майже миттєво
  const slow = await resolveAfter2Seconds();
  console.log(slow); // 2. цей рядок виконується через 2 секунди після 1.

  const fast = await resolveAfter1Second();
  console.log(fast); // 3. цей рядок виконується через 3 секунди після 1.
}

async function concurrentStart() {
  console.log('==ОДНОЧАСНИЙ ПОЧАТОК із "await"==');
  const slow = resolveAfter2Seconds(); // негайно запускає таймер
  const fast = resolveAfter1Second(); // негайно запускає таймер

  // 1. Виконання підходить до цього місця майже миттєво
  console.log(await slow); // 2. цей рядок виконується через 2 секунди після 1.
  console.log(await fast); // 3. цей рядок виконується через 2 секунди після 1., негайно за 2., оскільки швидкий проміс уже виконано
}

function concurrentPromise() {
  console.log('==ОДНОЧАСНИЙ ПОЧАТОК із "Promise.all"==');
  return Promise.all([resolveAfter2Seconds(), resolveAfter1Second()]).then(
    (messages) => {
      console.log(messages[0]); // повільно
      console.log(messages[1]); // швидко
    }
  );
}

async function parallel() {
  console.log('==ПАРАЛЕЛЬНЕ виконання з "await Promise.all"==');

  // Запускає 2 "завдання" паралельно, і очікує на завершення обох
  await Promise.all([
    (async () => console.log(await resolveAfter2Seconds()))(),
    (async () => console.log(await resolveAfter1Second()))(),
  ]);
}

sequentialStart(); // через 2 секунди друкує "повільно", потім іще за 1 секунду — "швидко"

// очікує на завершення коду вище
setTimeout(concurrentStart, 4000); // через 2 секунди друкує "повільно", і потім "швидко"

// знову очікує
setTimeout(concurrentPromise, 7000); // так само, як і concurrentStart

// і знову очікує
setTimeout(parallel, 10000); // справді паралельно: через 1 секунду друкує "швидко", а потім іще за одну секунду — "повільно"

Вираз await і паралелізм

У випадку послідовного старту у функції sequentialStart виконання зупиняється на 2 секунди на першому await, і потім іще на секунду на другому await. Другий таймер не створюється, поки не сплине час першого, тому загалом код завершує роботу за 3 секунди.

У випадку одночасного старту у функції concurrentStart обидва таймери створюються, а потім очікуються виразами await. Таймери виконують відлік одночасно, тобто код завершить роботу радше за 2, ніж за 3 секунди. Інакше кажучи — тоді, коли завершить відлік найповільніший таймер. Втім, виклики await все-таки виконуються послідовно, тобто другий await чекатиме на завершення першого. В цьому випадку результат швидшого таймера обробляється після завершення роботи повільнішого.

Якщо потрібно безпечним чином виконати паралельно два чи більше завдання, слід застосувати await до виклику Promise.all чи Promise.allSettled.

Застереження: Функції concurrentStart та concurrentPromise не є функціонально еквівалентними.

Якщо у функції concurrentStart швидкий проміс відхиляється перед сповненням повільного промісу, то буде викинуто помилку необробленого відхилення промісу, незалежно від наявності умови catch у місці виклику.

У функції concurrentPromise Promise.all зв'язує ланцюжок промісів за один прохід, тобто операція негайно завершить роботу в разі помилки незалежно від послідовності відхилення промісів, і помилка завжди відбуватиметься всередині налаштованого ланцюжка промісів, що дає змогу її обробити у звичайний спосіб.

Переписування ланцюжка промісів за допомогою асинхронної функції

API, який повертає проміс, буде в результаті утворювати ланцюжки промісів, і це розбиває функцію на декілька частин. Якщо розглянути наступний код:

function getProcessedData(url) {
  return downloadData(url) // повертає проміс
    .catch((e) => downloadFallbackData(url)) // повертає проміс
    .then((v) => processDataInWorker(v)); // повертає проміс
}

Це можна переписати як одну асинхронну функцію, як це показано нижче:

async function getProcessedData(url) {
  let v;
  try {
    v = await downloadData(url);
  } catch (e) {
    v = await downloadFallbackData(url);
  }
  return processDataInWorker(v);
}

Крім того, можна з'єднувати в ланцюжок проміси з catch():

async function getProcessedData(url) {
  const v = await downloadData(url).catch((e) => downloadFallbackData(url));
  return processDataInWorker(v);
}

Зауважте, що в обох версіях переписаної функції немає інструкції await після ключового слова return, хоча це теж було б вірно. Повернене з асинхронної функції значення неявно обгортається у Promise.resolve — якщо саме значення не є промісом (як це є в прикладах).

Специфікації

Сумісність із браузерами

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
async function statement
Chrome Full support 55
Edge Full support 15
Firefox Full support 52
Internet Explorer No support No
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

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