Async синтаксическая ошибка

I am trying to use async/await in NodeJS but my script is throwing a syntax error.

I was under the impression that async/await is supported naively since Node 7.6. When I run node -v I get v7.10.0.

Here is the contents of index.js:

async function getValueAsync() {
    return new Promise(function(resolve) {
        resolve('foo');
    });
}

let value = await getValueAsync();
console.log(value);

But when I invoke this script with node index.js I get:

let value = await getValueAsync();
                  ^^^^^^^^^^^^^
SyntaxError: Unexpected identifier
    at createScript (vm.js:53:10)
    at Object.runInThisContext (vm.js:95:10)
    at Module._compile (module.js:543:28)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:427:7)
    at startup (bootstrap_node.js:151:9)

I am running Linux Mint 18.1.

How can I get my script to compile and run?

asked May 21, 2017 at 14:25

Jonathan.Brink's user avatar

Jonathan.BrinkJonathan.Brink

23.4k19 gold badges72 silver badges112 bronze badges

await is only valid inside async functions, so you need, for example, an async IIFE to wrap your code with:

void async function() {
  let value = await getValueAsync();
  console.log(value);
}();

And, since return values from async functions are wrapped by a promise, you can shorten getValueAsync to simply this:

async function getValueAsync() {
  return 'foo';
}

Or don’t mark it as async and return a promise from it:

function getValueAsync() {
  return new Promise(function(resolve) {
    resolve('foo');
  });
}

answered May 21, 2017 at 14:46

robertklep's user avatar

robertkleprobertklep

197k34 gold badges393 silver badges380 bronze badges

2

JavaScript функции async и await – то, что важно понимать web-разработчику в 2019 году. В статье примеры кода и детальное погружение в тему.

Вначале были обратные вызовы.

Обратный вызов – функция, которая выполняется позднее.

Из-за асинхронной природы языка JavaScript обратные вызовы часто используются там, где результаты недоступны сразу.

Так выглядит асинхронное чтение файла в Node.js:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

Проблемы возникают, когда асинхронная операция не одна. Вот сценарий, где каждая операция асинхронная:

  • Делаем запрос в базу данных для пользователя Arfat.
  • Считываем profile_img_url и получаем изображение с someServer.com.
  • Далее преобразуем изображение в другой формат: PNG в JPEG.
  • Если преобразование получилось, отправляем пользователю электронное письмо.
  • И записываем эту задачу в наш файл transfors.log с отметкой времени.

Код выглядит так:

Глубокое погружение в асинхронные JavaScript функции

Обратите внимание на вложенность обратных вызовов и лестницу из }) в конце. Это ласково называется Ад обратных вызовов или Пирамида Судьбы (Pyramid of Doom). Главные недостатки:

  • Код становится труднее читать, потому что читать приходится слева направо.
  • Обработка ошибок сложна и часто приводит к ужасному коду.

Для решения этой проблемы боги JavaScript JS создали Promise. Теперь вместо вложенности обратных вызовов получаем цепочку.

Пример:

Глубокое погружение в асинхронные JavaScript функции

Поток стал привычным – сверху вниз, а не слева направо, как в обратных вызовах, что плюс. Тем не менее, с Promise по-прежнему проблемы:

  • Нуждаемся в обратном вызове для каждого .then.
  • Вместо try/catch приходится использовать .catch для обработки ошибок.
  • Организация циклов с множественными Promise в последовательности бросает вызов.

Для демонстрации последнего пункта примем этот вызов!

Задача

Предположим, цикл for выводит от 0 до 10 с произвольными интервалами (от 0 до n секунд). Требуется изменить поведение с использованием Promise так, чтобы числа печатались последовательно от 0 до 10. Например, если 0 отображается за 6 секунд, а 1 – за две секунды, то 1 ждёт печати 0 и так далее.

Само собой разумеется, не используйте JavaScript функции async и await или sort. Решение будет к концу.

После ES2017(ES8) JavaScript основы языка дополнились асинхронными функциями, которые упростили работу с Promise.

  • Асинхронные функции JavaScript работают поверх Promise.
  • Это не диаметрально другая концепция.
  • Функции рассматриваются как альтернативный способ написания кода на основе Promise.
  • С использованием async и await избегаем создания цепочки Promise.
  • В итоге получаем асинхронное выполнение при сохранении нормального синхронного подхода.

Следовательно, требуется понимание Promise для осознания концепции async/await.

Синтаксис

Здесь применяются два ключевых слова – async и await. async используется, чтобы сделать функцию асинхронной. Это разблокирует использование await внутри этих функций. Использование await в другом случае – синтаксическая ошибка.

// с объявлением функции
async function myFn() {
  // await ...
}
// со стрелочной функцией
const myFn = async () => {
  // await ...
}
function myFn() {
  // await fn(); (Синтаксическая ошибка, поскольку нет async) 
}

Видите async в начале объявления функции? Если функция стрелочная, async ставится после знака = и перед скобками.

Асинхронные функции используются и как методы объектов или в объявлениях класса. Это иллюстрируют JavaScript примеры:

// как метод объекта
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}
// в классе
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

Примечание: конструкторы классов, геттеры и сеттеры не могут быть асинхронными.

Семантика и выполнение

Асинхронные функции – обычные функции JavaScript с такими отличиями:

Асинхронные JavaScript функции всегда возвращают Promise.

async function fn() {
  return 'привет';
}
fn().then(console.log)
// привет

Функция fn возвращает 'привет'. Поскольку использовали async, возвращаемое значение 'привет' оборачивается в Promise посредством Promise.resolve.

Теперь посмотрим на эквивалентное альтернативное представление без использования async:

function fn() {
  return Promise.resolve('привет');
}
fn().then(console.log);
// привет

В этом случае вручную возвращаем Promise вместо использования async.

Точнее сказать, возвращаемое значение асинхронной функции JavaScript всегда оборачивается в Promise.resolve.

Для примитивов Promise.resolve возвращает обёрнутое в Promise значение. Но для объектов Promise возвращается тот же объект без оборачивания.

// для примитивных значений
const p = Promise.resolve('hello')
p instanceof Promise; 
// true
// p возвращается как есть
Promise.resolve(p) === p; 
// true

Что происходит, когда бросаем ошибку внутри асинхронной функции?

Например:

async function foo() {
  throw Error('bar');
}
foo().catch(console.log);

foo() вернёт отклонённый (rejected) Promise, если ошибку не перехватили. Вместо Promise.resolve Promise.reject оборачивает и возвращает ошибку. Смотрите раздел Обработка ошибок дальше.

В результате, что бы мы ни возвращали, всегда получаем Promise из асинхронной функции.

Асинхронные функции останавливаются на каждом await <выражение>.

await действует на выражение. Если выражение – Promise, выполнение асинхронной функции останавливается до получения результата Promise. Если выражение – другое значение, происходит преобразование в Promise с помощью Promise.resolve и выполнение resolve.

// функция, вызывающая задержку
// и получаем случайное значение
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};
async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
  
  return a + b * c;
}
// Выполнить fn
fn().then(console.log);

Теперь рассмотрим функцию fn построчно:

  • Когда выполняется fn, первой отработает строка const a = await 9;. Она внутри преобразуется в const a = await Promise.resolve(9);.
  • Поскольку используем await, fn делает паузу, пока переменная a не получит значение. В этом случае Promise назначит ей результат 9.
  • delayAndGetRandom(1000) заставляет fn приостанавливаться до тех пор, пока не выполнится функция delayAndGetRandom, что происходит через 1 секунду. Таким образом, fn делает паузу на 1 секунду.
  • Кроме того, delayAndGetRandom резолвится со случайным значением. Что бы ни передавалось в функцию resolve, значение присваивается переменной b.
  • c получает значение 5 аналогичным образом, и снова задержка на 1 секунду из-за await delayAndGetRandom(1000). В этом случае не используем конечное значение.
  • Наконец, вычисляем результат a + b * c, который обёрнут в Promise с использованием Promise.resolve. Эта обёртка возвращается.

Решение

Воспользуемся async/await для решения гипотетической задачи, поставленной в начале статьи:

Глубокое погружение в асинхронные JavaScript функции

Создаём асинхронную функцию finishMyTask и используем await для ожидания результата таких операций, как queryDatabase, sendEmail и logTaskInFile.

Если сравним с первым решением на базе Promise, обнаружим, что это примерно та же строчка кода. Тем не менее, async/await упростил синтаксис. Отсутствуют множественные обратные вызовы и .then/.catch.

Теперь решим задачу с числами, приведенную выше. Вот две реализации:

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));

// Реализация Один (С использованием цикла for)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});

// Реализация Два(С использованием рекурсии)

const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {

    if (i === 10) {
      return undefined;
    }

    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

Если хотите, запустите код самостоятельно в консоли repl.it.

Использование асинхронной функции с самого начала упростило бы задачу намного.

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

Обработка ошибок

Помните, что необработанная Error() оборачивается в отклонённый Promise? Несмотря на это, допускается использование try-catch в асинхронных функциях для синхронной обработки ошибок. Начнём с этой служебной функции:

async function canRejectOrReturn() {
  // подождать одну секунду
  await new Promise(res => setTimeout(res, 1000));
// Отклонить с вероятностью ~ 50%
  if (Math.random() > 0.5) {
    throw new Error('Извините, слишком большое число.')
  }
return 'идеальное число';
}

canRejectOrReturn() – асинхронная функция, которая либо выполняется с результатом 'идеальное число', либо отклоняется с Error('Извините, слишком большое число').

Смотрите пример кода:

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'ошибка перехвачена';
  }
}

Поскольку ожидаем canRejectOrReturn, его собственное отклонение превращается в ошибку, и блок catch выполняется. То есть, foo завершится либо с результатом undefined (потому что ничего не возвращаем в try), либо с 'ошибка перехвачена'. Отклонения не произойдёт, так как использовали блок try-catch для обработки ошибки внутри функции foo.

Ещё один пример:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'ошибка перехвачена';
  }
}

На этот раз возвращаем (а не ожидаем) canRejectOrReturn из foo. foo либо выполнится с результатом 'идеальное число', либо отклонится с Error('Извините, слишком большое число'). Блок catch не будет выполнен.

Почему так? Просто возвращаем Promise, который вернул canRejectOrReturn. Следовательно, выполнение foo становится выполнением canRejectOrReturn. Разделим return canRejectOrReturn() на две строки для большей ясности. Обратите внимание на отсутствие await в первой строке:

try {
    const promise = canRejectOrReturn();
    return promise;
}

И посмотрим, как использовать await и return вместе:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'ошибка перехвачена';
  }
}

В этом случае foo завершится либо с результатом 'идеальное число', либо с 'ошибка перехвачена'. Здесь нет отклонения. Это как первый пример, только с await. За исключением того, что получаем значение, которое создаёт canRejectOrReturn, а не undefined.

Прервём return await canRejectOrReturn();, чтобы увидеть эффект:

try {
    const value  = await canRejectOrReturn();
    return value;
}
// ...

Распространённые ошибки и подводные камни

Отсутствие await

Иногда забываем добавить ключевое слово await перед Promise или вернуть его. Вот пример:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

Обратите внимание, что не используется await или return. foo всегда завершается с результатом undefined без ожидания 1 секунду. Тем не менее, Promise начинает выполнение. Это запустит побочные эффекты. Если появится ошибка или отклонение, будет выдано UnhandledPromiseRejectionWarning.

Асинхронные функции в обратных вызовах

Часто используем асинхронные функции в .map или .filter в качестве обратных вызовов. Рассмотрим пример. Предположим, функция fetchPublicReposCount(username) извлекает количество общедоступных GitHub-репозиториев пользователя. Три пользователя для обработки. Посмотрим код:

const url = 'https://api.github.com/users';
// функция для получения количества репозиториев
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

Хотим получить количество репозиториев ['ArfatSalman', 'octocat', 'norvig']. Сделаем так:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Обратите внимание на async в обратном вызове .map. Ожидаем, что переменная counts будет содержать количество репов. Но асинхронные функции возвращают Promise. Следовательно, counts на самом деле – массив из Promise. .map запускает анонимный обратный вызов для каждого username, и при каждом вызове возвращается Promise, который .map хранит в результирующем массиве.

Слишком последовательное использование await

Смотрите на такое решение:

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}

Вручную получаем каждое количество и добавляем в массив counts. Проблема этого кода в том, что пока не будет получено количество для первого пользователя, следующее не запустится. За один раз выбирается только одно количество репов.

Если для одной выборки требуется 300 мс, то fetchAllCounts будет занимать ~ 900 мс для 3 пользователей. Как видим, время линейно растёт с увеличением количества пользователей. Поскольку выборка репов не взаимозависимая, распараллелим операцию.

Получаем пользователей одновременно, а не последовательно с использованием .map и Promise.all.

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.all принимает массив Promise на входе и возвращает Promise на выходе. Конечный Promise получает массив результатов всех Promise или становится rejected при первом отклонении. Для частичного параллелизма смотрите p-map.

Заключение

С введением асинхронных итераторов асинхронные функции получат ещё большее распространение. Тем, кто изучает программирование JavaScript, важно понимание этих концепций. Надеемся, что статья прольёт свет на await и async.

Оригинал

А с какими проблемами в асинхронном программировании сталкивались вы?

Async/await

Существует специальный синтаксис для работы с промисами, который называется «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 ждать до тех пор, пока промис справа от await не выполнится. После чего оно вернёт его результат, и выполнение кода продолжится.

В этом примере промис успешно выполнится через 1 секунду:

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("готово!"), 1000)
  });

*!*
  let result = await promise; // будет ждать, пока промис не выполнится (*)
*/!*

  alert(result); // "готово!"
}

f();

В данном примере выполнение функции остановится на строке (*) до тех пор, пока промис не выполнится. Это произойдёт через секунду после запуска функции. После чего в переменную result будет записан результат выполнения промиса, и браузер отобразит alert-окно «готово!».

Обратите внимание, хотя await и заставляет JavaScript дожидаться выполнения промиса, это не отнимает ресурсов процессора. Пока промис не выполнится, JS-движок может заниматься другими задачами: выполнять прочие скрипты, обрабатывать события и т.п.

По сути, это просто «синтаксический сахар» для получения результата промиса, более наглядный, чем promise.then.

««warn header=»await нельзя использовать в обычных функциях»
Если мы попробуем использовать `await` внутри функции, объявленной без `async`, получим синтаксическую ошибку:

function f() {
  let promise = Promise.resolve(1);
*!*
  let result = await promise; // SyntaxError
*/!*
}

Ошибки не будет, если мы укажем ключевое слово async перед объявлением функции. Как было сказано раньше, await можно использовать только внутри async–функций.


Давайте перепишем пример `showAvatar()` из раздела <info:promise-chaining> с помощью `async/await`:

1. Нам нужно заменить вызовы `.then` на `await`.
2. И добавить ключевое слово `async` перед объявлением функции.

```js run
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();
```

Получилось очень просто и читаемо, правда? Гораздо лучше, чем раньше.

````smart header="`await` нельзя использовать на верхнем уровне вложенности"
Программисты, узнав об `await`, часто пытаются использовать эту возможность на верхнем уровне вложенности (вне тела функции). Но из-за того, что `await` работает только внутри `async`–функций, так сделать не получится:

```js run
// SyntaxError на верхнем уровне вложенности
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
```

Можно обернуть этот код в анонимную `async`–функцию, тогда всё заработает:

```js
(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  ...
})();
```


««smart header=»await работает с «thenable»–объектами»
Как и `promise.then`, `await` позволяет работать с промис–совместимыми объектами. Идея в том, что если у объекта можно вызвать метод `then`, этого достаточно, чтобы использовать его с `await`.

В примере ниже, экземпляры класса Thenable будут работать вместе с await:

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // выполнить resolve со значением this.num * 2 через 1000мс
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
};

async function f() {
  // код будет ждать 1 секунду,
  // после чего значение result станет равным 2
  let result = await new Thenable(1);
  alert(result);
}

f();

Когда await получает объект с .then, не являющийся промисом, JavaScript автоматически запускает этот метод, передавая ему аргументы – встроенные функции resolve и reject. Затем await приостановит дальнейшее выполнение кода, пока любая из этих функций не будет вызвана (в примере это строка (*)). После чего выполнение кода продолжится с результатом resolve или reject соответственно.


````smart header="Асинхронные методы классов"
Для объявления асинхронного метода достаточно написать `async` перед именем:

```js run
class Waiter {
*!*
  async wait() {
*/!*
    return await Promise.resolve(1);
  }
}

new Waiter()
  .wait()
  .then(alert); // 1
```
Как и в случае с асинхронными функциями, такой метод гарантированно возвращает промис, и в его теле можно использовать `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();

В случае ошибки выполнение try прерывается и управление прыгает в начало блока catch. Блоком try можно обернуть несколько строк:

async function f() {

  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // перехватит любую ошибку в блоке try: и в fetch, и в response.json
    alert(err);
  }
}

f();

Если у нас нет try..catch, асинхронная функция будет возвращать завершившийся с ошибкой промис (в состоянии rejected). В этом случае мы можем использовать метод .catch промиса, чтобы обработать ошибку:

async function f() {
  let response = await fetch('http://no-such-url');
}

// f() вернёт промис в состоянии rejected
*!*
f().catch(alert); // TypeError: failed to fetch // (*)
*/!*

Если забыть добавить .catch, то будет сгенерирована ошибка «Uncaught promise error» и информация об этом будет выведена в консоль. Такие ошибки можно поймать глобальным обработчиком, о чём подробно написано в разделе info:promise-error-handling.

«`smart header=»async/await и `promise.then/catch`»
При работе с `async/await`, `.then` используется нечасто, так как `await` автоматически ожидает завершения выполнения промиса. В этом случае обычно (но не всегда) гораздо удобнее перехватывать ошибки, используя `try..catch`, нежели чем `.catch`.

Но на верхнем уровне вложенности (вне async–функций) await использовать нельзя, поэтому .then/catch для обработки финального результата или ошибок – обычная практика.

Так сделано в строке (*) в примере выше.


````smart header="`async/await` отлично работает с `Promise.all`"
Когда необходимо подождать несколько промисов одновременно, можно обернуть их в `Promise.all`, и затем `await`:

```js
// await будет ждать массив с результатами выполнения всех промисов
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
]);

В случае ошибки она будет передаваться как обычно: от завершившегося с ошибкой промиса к Promise.all. А после будет сгенерировано исключение, которое можно отловить, обернув выражение в try..catch.


## Итого

Ключевое слово `async` перед объявлением функции:

1. Обязывает её всегда возвращать промис.
2. Позволяет использовать `await` в теле этой функции.

Ключевое слово `await` перед промисом заставит JavaScript дождаться его выполнения, после чего:

1. Если промис завершается с ошибкой, будет сгенерировано исключение, как если бы на этом месте находилось `throw`.
2. Иначе вернётся результат промиса.

Вместе они предоставляют отличный каркас для написания асинхронного кода. Такой код легко и писать, и читать.

Хотя при работе с `async/await` можно обходиться без `promise.then/catch`, иногда всё-таки приходится использовать эти методы (на верхнем уровне вложенности, например). Также `await` отлично работает в сочетании с `Promise.all`, если необходимо выполнить несколько задач параллельно.

Update: This post was originally published on my blog decodingweb.dev, where you can read the latest version for a 💯 user experience. ~reza

Await is only valid in Async functions; This syntax error occurs when you use an await expression outside an async execution context, like an async function or top-level body of an ES module (top-level await).

Here’s what it looks like:

Image description

How to solve “await is only valid in async functions” error?

Await expressions are used in two ways:

  1. Using await expressions in async functions
  2. top-level await

Using await expressions in async functions: If you’re using an await expression in a function, adding the async keyword to the function declaration resolves the issue.

// ⛔ Wrong
function getBooks() {
    let books = await fetch('some-url/api/v1/books')
}

// ✅ Correct
async function getBooks() {
    let books = await fetch('some-url/api/v1/books')
}

Enter fullscreen mode

Exit fullscreen mode

Just beware that once you make a function async, it’ll always return a promise. So if you’re using the respective function in other places, remember to update your code accordingly.

You can also make callback functions async:

setTimeout(async () => {
    const items = await getItems()
}, 2000)

Enter fullscreen mode

Exit fullscreen mode

Top-level await: If your await statement isn’t in a function, you can wrap your code in an async IIFE (Immediately Invoked Function Expression):

(async () => {
   let books = await fetch('some-url/api/v1/books')
   // Any code here will be executed after the books variable has the value.
})()

Enter fullscreen mode

Exit fullscreen mode

Await can be used on its own in ES modules too. If you’re using Node js, you must set Node’s module system to ES module system first; Top-level await isn’t supported by the Node.js default module system (CommonJS).

To do that, add "type": "module" to your package.json file. If you don’t have a package.json file yet, run the following terminal command from your project directory:

npm init

Enter fullscreen mode

Exit fullscreen mode

Then add "type": "module" to your module’s configuration:

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "type": "module",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "",
  "license": "ISC"
} 

Enter fullscreen mode

Exit fullscreen mode

If your module is supposed to be loaded in a browser, add "type=module" to the <script> tag, and you’re good to go:

<script type="module" src="app.js"></script>

Enter fullscreen mode

Exit fullscreen mode

If you’re curious how async/await works, please read on.

Understanding async functions in JavaScript

Async functions are easy to spot as they have the async keyword in their declaration.

async function myFunction () {
  // await can be used here ...
}

Enter fullscreen mode

Exit fullscreen mode

An async function always returns its value wrapped in a Promise (if the returned value isn’t already a promise). You can access the returned value once it’s resolved (if not rejected).

Let’s see an example:

async function asyncTest() {
    return 1
}

let asyncFunctionValue = asyncTest()
console.log(asyncFunctionValue)
// output: Promise { 1 }

// Get the value when it's resolved
asyncFunctionValue
  .then(value => {
    console.log(value)
   // output: 1
  })

Enter fullscreen mode

Exit fullscreen mode

So basically, the async keyword implicitly wraps the returned value in a promise (if it’s not already a promise).

The above code is equivalent to the following:

function asyncTest() {
  let returnValue = 'someValue' 
  return new Promise.resolve(returnValue)
}

Enter fullscreen mode

Exit fullscreen mode

Now, what’s the await keyword?

The async/await duo enable you to write asynchronous code more cleanly by avoiding promise chains (a cascade of then() methods).

promiseObj
.then(value => {
// some code here
})
    .then(value => {
    // some code here
    })
        .then (value => {
        // some code here
        })
            .then(value => {
            // some code here
            })

Enter fullscreen mode

Exit fullscreen mode

The await keyword makes JavaScript look synchronous, even though it never blocks the main thread. The purpose of using await inside an async function is to write cleaner asynchronous code in promise-based APIs, like the Fetch API.

The rule is, await expressions must be used inside async functions. Otherwise, you’ll get the syntax error «await is only valid in async functions and the top level bodies of modules».

Let’s make it clear with an example.

Using the fetch API in the old-fashioned way is like this:

fetch('some-url/api/v1/movies')
  .then(response => response.json())
  .then(data => console.log(data))

Enter fullscreen mode

Exit fullscreen mode

But with async/await, you won’t need then() callbacks:

let response
(async () =>  {
     let movies = await fetch('some-url/api/v1/movies')
     // The code following await is treated as if they are in a then() callback
     response = await movies.json()
})()

Enter fullscreen mode

Exit fullscreen mode

So when JavaScript encounters an await expression in your async function, it pauses the execution of the code following await and gets back to the caller that invoked the async function. The code following await is pushed to a microtask queue to be executed once the promise being awaited is resolved.

The following code is a simplified (and imaginary) chatbot that starts a chat session with a user. We have a function named say(), which returns messages after a delay (to mimic human typing).

function say(text, delay = 500) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(text)
        }, delay)
    })
}

async function startConversation() {
    console.log('Hi!')   
    console.log(await say('my name is R2B2,'))
    console.log(await say('How can I help you?'))
}

startConversation()
console.log('Please input your email:')

Enter fullscreen mode

Exit fullscreen mode

However, the function doesn’t return the messages in the order we expect:

Hi!
Please input your email:
my name is R2B2,
How can I help you?

Enter fullscreen mode

Exit fullscreen mode

The reason is once JavaScript gets to the first await, it pauses the execution of what’s left in the function and returns to the caller (startConveration()). The main thread that is now freed, prints the "Please input your email" message.

And once the promises are resolved, the function’s remaining lines are executed — as if they were inside a callback function.

It’s just the good old then() callback functionality but more cleanly!

Additionally, the async/await duo lets us use try/catch with promised-based APIs. Something you couldn’t simply do with then() callbacks.

let items = []
try {
    items = await getItemsFromApi()
} catch (error) {
   // Handle the error here
}

Enter fullscreen mode

Exit fullscreen mode

A quick note on performance

Since the code after the await is to be paused execution, you gotta make sure it’s only followed by the code that depends on it. Otherwise, some operations will have to wait for no reason.

Imagine we need to get the best deals from Amazon and Etsy and merge the results into an array (to be listed on a web page).

The following approach isn’t optimized:

function getAmazonDeals() {
    // get Amazon deals ...
}

function getEtsyDeals() {
    // get Etsy deals ...
}


// Using an IEEF function here ...
(async () => {
    const amazonDeals = await getAmazonDeals(1000)
    const etsyDeals = await  getEtsyDeals(1000)

    const allDeals = [...amazonDeals, ...etsyDeals]
    populateDealsList(allDeals)

})()

Enter fullscreen mode

Exit fullscreen mode

In the above example, the lines following the first await are paused until the data is fetched from Amazon. This means the second request (getEtsyDeals()) has to wait without being dependent on the return value of getAmazonDeals().

So if each request takes one second, fetching deals from Amazon and Etsy would take two seconds in total.

But what if we initiated both requests concurrently and use await afterward?

Let’s see how:

function getAmazonDeals() {
    // get Amazon deals ...
}

function getEtsyDeals() {
    // get Etsy deals ...
}


// Using an IEEF function here ...
(async () => {
    // We're not using await here to initiate the requests immediately
    const amazonDeals = getAmazonDeals(1000)
    const etsyDeals = getEtsyDeals(1000)

     // Since the result of both requests are still promises, we use await when we want to combine them into one array
    // The leads aren't populated until we have deals from both sources
    const allDeals = [...await amazonDeals, ...await etsyDeals]
    populateDealsList(allDeals)
})()

Enter fullscreen mode

Exit fullscreen mode

Since both requests start immediately, we have both responses in one second.

I hope you found this quick guide helpful.

Thanks for reading.

Callback — это не что-то замысловатое или особенное, а просто функция, вызов которой отложен на неопределённое время. Благодаря асинхронному характеру JavaScript, обратные вызовы нужны были везде, где результат не может быть получен сразу.

Ниже приведён пример асинхронного чтения файла на Node.js:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

Проблемы начинаются, когда нужно выполнить несколько асинхронных операций. Просто представьте себе подобный сценарий:

  • Выполняется запрос в БД на некого пользователя Arfat. Нужно считать его поле profile_img_url и загрузить соответствующее изображение с сервера someServer.ru.
  • После загрузки изображения необходимо его конвертировать, допустим из PNG в JPEG.
  • В случае успешной конвертации нужно отправить письмо на почту пользователя.
  • Это событие нужно занести в файл transformations.log и указать дату.
queryDatabase({ username: 'Arfat'}, (err, user) => {
  // Обработка ошибок при запросе в БД
  const image_url = user.profile_img_url;
  getImageByURL('someServer.com/q=${image_url}', (err, image) => {
    // Обработка ошибок получения изображения
    transformImage(image, (err, transformedImage) => {
      // Обработка ошибок конвертирования 
      sendEmail(user.email, (err) => {
        // Обработка ошибок отсылки по почте
        logTaskInFile('Конвертирование файла и отсылка по почте', (err) 
          // Обработка ошибок лога
        })
      })
    })
  })
})

Обратите внимание на вложенность обратных вызовов и пирамиду из }) в конце. Подобные случаи принято называть Callback Hell или Pyramid of Doom. Вот основные недостатки:

  • Такой код сложно читать.
  • В таком коде сложно обрабатывать ошибки и одновременно сохранять его «качество».

Для решения этой проблемы в JavaScript были придуманы промисы (англ. promises). Теперь глубокую вложенность коллбэков можно заменить ключевым словом then:

queryDatabase({ username: 'Arfat'})
  .then((user) => {
    const image_url = user.profile_img_url;
    return getImageByURL('someServer.com/q=${image_url}') 
      .then(image => transformImage(image))
      .then(() => sendEmail(user.email))
})
.then(() => logTaskInFile('...'))
.catch(() => handleErrors()) // Обработка ошибок

Код стал читаться сверху вниз, а не слева направо, как это было в случае с обратными вызовами. Это плюс к читаемости. Однако и у промисов есть свои проблемы:

  • Всё ещё нужно работать с кучей .then.
  • Вместо обычного try/catch нужно использовать .catch для обработки всех ошибок.
  • Работа с несколькими промисами в цикле не всегда интуитивно понятна и местами сложна.

В качестве демонстрации последнего пункта попробуйте выполнить такое задание:

Предположим, что у вас есть цикл for, который выводит последовательность чисел от 0 до 10 со случайным интервалом (от 0 до n секунд). Используя промисы нужно изменить цикл так, чтобы числа выводились в строгой последовательности от 0 до 10. К примеру, если вывод нуля занимает 6 секунд, а единицы 2 секунды, то единица должна дождаться вывода нуля и только потом начать свой отсчёт (чтобы соблюдать последовательность).

Стоит ли говорить, что в решении этой задачи нельзя использовать конструкцию async/await либо .sort функцию? Решение будет в конце.


Добавление async-функций в ES2017 (ES8) сделало работу с промисами легче.

  • Важно отметить, что async-функции работают поверх промисов.
  • Эти функции не являются принципиально другими концепциями.
  • Async-функции были задуманы как альтернатива коду, использующему промисы.
  • Используя конструкцию async/await, можно полностью избежать использование цепочек промисов.
  • С помощью async-функций возможно организовать работу с асинхронным кодом в синхронном стиле.

Как видите, знание промисов всё же необходимо для понимания работы async/await.

Синтаксис

Синтаксис состоит из двух ключевых слов: async и await. Первое делает функцию асинхронной. Именно в таких функциях разрешается использование await. Использование await в любом другом случае вызовет ошибку.

// В объявлении функции
async function myFn() {
  // await ...
}

// В стрелочной функции
const myFn = async () => {
  // await ...
}

function myFn() {
  // await fn(); (синтаксическая ошибка, т. к. нет async)
}

Обратите внимание, что async вставляется в начале объявления функции, а в случае стрелочной функции — между знаком = и скобками.

Async-функции могут быть помещены в объект в качестве методов или же просто использоваться в объявлении класса.

// В качестве метода объекта
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}

// В самом классе
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

Примечание Конструкторы класса и геттеры/сеттеры не могут быть асинхронными.

Семантика и правила выполнения

Async-функции похожи на обычные функции в JavaScript, за исключением нескольких вещей:

Async-функции всегда возвращают промисы

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

Функция fn возвращает строку 'hello'. Т. к. это асинхронная функция, значение строки обёртывается в промис (с помощью конструктора).

Код выше можно переписать и без использования async:

function fn() {
  return Promise.resolve('hello');
}
fn().then(console.log);
// hello

В таком случае, вместо async, код вручную возвращает промис.

Тело асинхронной функции всегда обёртывается в новый промис

Если возвращаемое значение является примитивом, async-функция возвращает это значение, обёрнутое в промис. Но если возвращаемое значение и есть объект промиса, его решение возвращается в новом промисе.

// В случае примитивного типа значения
const p = Promise.resolve('hello')
p instanceof Promise; 
// true

// p возвращается как есть

Promise.resolve(p) === p; 
// true

Что происходит, когда внутри асинхронной функции возникает какая-нибудь ошибка?

async function foo() {
  throw Error('bar');
}

foo().catch(console.log);

Если ошибка не будет обработана, foo() вернёт промис с реджектом. В таком случае вместо Promise.resolve вернётся Promise.reject, содержащий ошибку.

Суть async-функций в том, что что бы вы не возвращали, на выходе вы всегда будете получать промис.

Асинхронные функции приостанавливаются при каждом await выражении

await сказывается на выражениях. Если выражение является промисом, то async-функция будет приостановлена до тех пор, пока промис не выполнится. Если же выражение не является промисом, то оно конвертируется в промис через Promise.resolve и потом завершается.

// Функция задержки
// с возвращением случайного числа
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};

async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
  
  return a + b * c;
}

// Вызов fn
fn().then(console.log);

Как работает fn функция?

  1. После вызова fn функции первая строка конвертируется из const a = await 9; в const a = await Promise.resolve(9);.
  2. После использования await, выполнение функции приостанавливается, пока a не получит своё значение (в данном случае это 9).
  3. delayAndGetRandom(1000) приостанавливает выполнение fn функции, пока не завершится сама (после 1 секунды). Это, фактически, можно назвать остановкой fn функции на 1 секунду.
  4. Также delayAndGetRandom(1000) через resolve возвращает случайное значение, которое присваивается переменной b.
  5. Случай с переменной c идентичен случаю переменной a. После этого опять происходит пауза на 1 секунду, но теперь delayAndGetRandom(1000) ничего не возвращает, т. к. этого не требуется.
  6. Под конец эти значения считаются по формуле a + b * c. Результат обёртывается в промис с помощью Promise.resolve и возвращается функцией.

Примечание Если такие паузы напоминают вам генераторы в ES6, то на это есть свои причины.

Решение задачи

Вот решение задачи, поставленной в начале статьи, с использованием async/await.

async function finishMyTask() {
  try {
    const user = await queryDatabase({ username: 'Arfat' }); 
    const image_url = user.profile_img_url;
    const image = await getImageByURL('someServer.com/q=${image_url}'); 
    const transformedlmage = await transformImage(image); 
    await sendEmail(user.email); 
    await logTaskInFile(' ... ');
  } catch(err) {
    // Обработка всех ошибок
  }
}

В функции finishMyTask используется await для ожидания результатов таких операций, как queryDatabase, sendEmail, logTaskInFile и т. д. Если сравнить это решение с решением, использовавшим промисы, то вы обратите внимание на их сходство. Однако версия с async/await упрощает синтаксические сложности. В этом способе нет кучи коллбэков и цепочек .then/.catch.

Вот то решение с выводом чисел. Тут есть два способа:

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));

// Решение #1 (с использованием цикла for)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});

// Решение #2 (с использованием рекурсии)

const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {

    if (i === 10) {
      return undefined;
    }

    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

С использованием async-функций решение поставленной задачи упрощается до безобразия:

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

Обработка ошибок

Как было сказано выше, необработанные ошибки обёртываются в неудачный (rejected) промис. Но в async-функциях всё ещё можно использовать конструкцию try-catch для синхронной обработки ошибок.

async function canRejectOrReturn() {
  // Ждём секунду
  await new Promise(res => setTimeout(res, 1000));
// Реджектим в 50% случае
  if (Math.random() > 0.5) {
    throw new Error('Простите, число больше, чем нужно.')
  }

return 'Число подошло';
}

canRejectOrReturn() — это асинхронная функция, которая будет удачно завершатся с 'Число подошло', либо неудачно завершаться с Error('Простите, число больше, чем нужно.').

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'Ошибка обработана';
  }
}

Поскольку в коде выше ожидается выполнение canRejectOrReturn, то его собственное неудачное завершение вызовет исполнение блока catch. Поэтому функция foo завершится либо с undefined (т. к. в блоке try ничего не возвращается), либо с 'Ошибка обработана'. Поэтому у этой функции не будет неудачного завершения, т. к. try-catch блок будет обрабатывать  ошибку самой функции foo.

Вот другой пример:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'Ошибка обработана';
  }
}

Обратите внимание, что в коде выше из foo возвращается (без ожидания) canRejectOrReturn. foo завершится либо с 'число подошло', либо с реджектом Простите, число больше, чем нужно.‘). Блок catch никогда не будет исполняться.

Это происходит из-за того, что foo возвращает промис, который передан от canRejectOrReturn. Следовательно, решение функции foo становится решением canRejectOrReturn. Такой код можно представить всего в двух строках:

try {
    const promise = canRejectOrReturn();
    return promise;
}

Вот что получится, если использовать await и return разом:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'Ошибка обработана';
  }
}

В коде выше foo будет удачно завершаться и с  'число подошло', и с 'Ошибка обработана'. В таком коде реджектов не будет. Но в отличие от одного из примеров выше, foo завершится со значением canRejectOrReturn, а не с undefined.

Вы можете убедиться в этом сами, убрав строку return await canRejectOrReturn():

try {
    const value  = await canRejectOrReturn();
    return value;
}
// ...

Популярные ошибки и подводные камни

Из-за сложных манипуляций с промисами и async/await концепциями вы можете встретиться с различными тонкостями, что может привести к ошибкам.

Не забывайте await

Частая ошибка заключается в том, что перед промисом забывается ключевое слово await:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'Обработка';
  }
}

Обратите внимание, здесь не используется ни await, ни return. Функция foo всегда будет завершаться с undefined (без задержки в 1 секунду). Тем не менее, промис будет выполняться. Если промис будет выдавать ошибку либо реджект, то будет вызываться UnhandledPromiseRejectionWarning.

async-функции в обратных вызовах

async-функции часто используются в .map или .filter в качестве коллбэков. Вот пример — допустим, существует функция fetchPublicReposCount(username), которая возвращает количество открытых репозиториев на GitHub. Есть 3 пользователя, чьи показатели нужно взять. Используется такой код:

const url = 'https://api.github.com/users';

// Получает количество открытых репозиториев
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

И для того, чтобы получить количество репозиториев пользователей (['ArfatSalman', 'octocat', 'norvig']), код должен выглядеть как-то так:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];

const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Обратите внимание на слово await в обратном вызове функции .map. Можно было бы ожидать, что переменная counts будет содержать число — количество репозиториев. Но как было сказано ранее, все async-функции возвращают промисы. Следовательно, counts будет массивом промисов. .map вызывает анонимной коллбэк для каждого пользователя.

Слишком последовательное использование await

Допустим, есть такой код:

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}

В переменную count помещается количество репозиториев, потом это количество добавляется в массив counts. Проблема этого кода в том, что пока с сервера не придут данные первого пользователя, все последующие пользователи будут находиться в ожидании. Получается, что в один момент времени обрабатывается только один пользователь.

Если на обработку одного пользователя будет уходить 300 мс, то на всех пользователей уйдёт почти секунда. В этом случае затрачиваемое время будет линейно зависеть от количества пользователей. Поскольку получение количества репозиториев не зависит друг от друга, то можно распараллелить эти процессы. Тогда пользователи будут обрабатываться одновременно, а не последовательно. Для этого понадобятся .map и Promise.all.

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.all на входе получает массив промисов и возвращает промис. Возвращаемый промис завершается после окончания всех промисов в массиве либо при первом реджекте. Возможно, все эти промисы не запустятся строго одновременно. Чтобы добиться строгого параллелизма, взгляните на p-map. А если нужно, чтобы async-функции были более адаптивными, посмотрите на Async Iterators.

Перевод статьи «Deeply Understanding JavaScript Async and Await with Examples»

Понравилась статья? Поделить с друзьями:

Не пропустите эти материалы по теме:

  • Яндекс еда ошибка привязки карты
  • Asustpcenter exe ошибка приложения 0xc000012d
  • Asus сервисы google play произошла ошибка
  • Asterisk ошибка 403
  • Asterisk 503 ошибка

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии