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.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
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
с отметкой времени.
Код выглядит так:
Обратите внимание на вложенность обратных вызовов и лестницу из })
в конце. Это ласково называется Ад обратных вызовов или Пирамида Судьбы (Pyramid of Doom). Главные недостатки:
- Код становится труднее читать, потому что читать приходится слева направо.
- Обработка ошибок сложна и часто приводит к ужасному коду.
Для решения этой проблемы боги JavaScript JS создали Promise. Теперь вместо вложенности обратных вызовов получаем цепочку.
Пример:
Поток стал привычным – сверху вниз, а не слева направо, как в обратных вызовах, что плюс. Тем не менее, с 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
для решения гипотетической задачи, поставленной в начале статьи:
Создаём асинхронную функцию 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:
How to solve “await is only valid in async functions” error?
Await expressions are used in two ways:
- Using await expressions in async functions
- 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
функция?
- После вызова
fn
функции первая строка конвертируется изconst a = await 9;
вconst a = await Promise.resolve(9);
. - После использования
await
, выполнение функции приостанавливается, покаa
не получит своё значение (в данном случае это 9). delayAndGetRandom(1000)
приостанавливает выполнениеfn
функции, пока не завершится сама (после 1 секунды). Это, фактически, можно назвать остановкойfn
функции на 1 секунду.- Также
delayAndGetRandom(1000)
черезresolve
возвращает случайное значение, которое присваивается переменнойb
. - Случай с переменной
c
идентичен случаю переменнойa
. После этого опять происходит пауза на 1 секунду, но теперьdelayAndGetRandom(1000)
ничего не возвращает, т. к. этого не требуется. - Под конец эти значения считаются по формуле
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»