Here’s some code:
import 'babel-polyfill'
async function helloWorld () {
throw new Error ('hi')
}
helloWorld()
I also went deep and tried this as well:
import 'babel-polyfill'
async function helloWorld () {
throw new Error ('hi')
}
async function main () {
try {
await helloWorld()
} catch (e) {
throw e
}
}
main()
and:
import 'babel-polyfill'
async function helloWorld () {
throw new Error ('hi')
}
try {
helloWorld()
} catch (e) {
throw e
}
This works:
import 'babel-polyfill'
async function helloWorld () {
throw new Error('xxx')
}
helloWorld()
.catch(console.log.bind(console))
asked Nov 6, 2015 at 8:18
ThomasReggiThomasReggi
54.1k85 gold badges234 silver badges418 bronze badges
2
async
is meant to be used with Promises. If you reject the promise, then you can catch
the error, if you resolve the promise, that becomes the return value of the function.
async function helloWorld () {
return new Promise(function(resolve, reject){
reject('error')
});
}
try {
await helloWorld();
} catch (e) {
console.log('Error occurred', e);
}
answered Nov 6, 2015 at 14:55
Ruan MendesRuan Mendes
89.6k31 gold badges152 silver badges215 bronze badges
5
So it’s kind of tricky, but the reason you’re not catching the error is because, at the top level, the entire script can be thought of as a synchronous function. Anything you want to catch asynchronously needs to be wrapped in an async
function or using Promises.
So for instance, this will swallow errors:
async function doIt() {
throw new Error('fail');
}
doIt();
Because it’s the same as this:
function doIt() {
return Promise.resolve().then(function () {
throw new Error('fail');
});
}
doIt();
At the top level, you should always add a normal Promise-style catch() to make sure that your errors get handled:
async function doIt() {
throw new Error('fail');
}
doIt().catch(console.error.bind(console));
In Node, there is also the global unhandledRejection
event on process that you can use to catch all Promise errors.
answered Nov 6, 2015 at 15:11
nlawsonnlawson
11.5k4 gold badges40 silver badges48 bronze badges
To catch an error from an async function, you can await the error:
async function helloWorld () {
//THROW AN ERROR FROM AN ASYNC FUNCTION
throw new Error('hi')
}
async function main() {
try {
await helloWorld()
} catch(e) {
//AWAIT THE ERROR WITHIN AN ASYNC FUNCTION
const error = await e
console.log(error)
}
}
main()
Alternatively, you can just await the error message:
async function main() {
try {
await helloWorld()
} catch(e) {
//AWAIT JUST THE ERROR MESSAGE
const message = await e.message
console.log(message)
}
}
answered Jan 14 at 15:17
Время на прочтение
8 мин
Количество просмотров 65K
Конструкция async/await появилась в стандарте ES7. Её можно считать замечательным улучшением в сфере асинхронного программирования на JavaScript. Она позволяет писать код, который выглядит как синхронный, но используется для решения асинхронных задач и не блокирует главный поток. Несмотря на то, что async/await — это отличная новая возможность языка, пользоваться ей правильно не так уж и просто. Материал, перевод которого мы публикуем сегодня, посвящён разностороннему исследованию async/await и рассказу о том, как использовать этот механизм правильно и эффективно.
Сильные стороны async/await
Самое важное преимущество, которое получает программист, пользующийся конструкцией async/await, заключается в том, что она даёт возможность писать асинхронный код в стиле, характерном для синхронного кода. Сравним код, написанный с использованием async/await, и код, основанный на промисах.
// async/await
async getBooksByAuthorWithAwait(authorId) {
const books = await bookModel.fetchAll();
return books.filter(b => b.authorId === authorId);
}
// промис
getBooksByAuthorWithPromise(authorId) {
return bookModel.fetchAll()
.then(books => books.filter(b => b.authorId === authorId));
}
Несложно заметить, что async/await-версия примера получилась более понятной, чем его вариант, в котором использован промис. Если не обращать внимания на ключевое слово await
, этот код будет выглядеть как обычный набор инструкций, выполняемых синхронно — как в привычном JavaScript или в любом другом синхронном языке вроде Python.
Привлекательность async/await обеспечивается не только улучшением читабельности кода. Этот механизм, кроме того, пользуется отличной поддержкой браузеров, не требующей каких-либо обходных путей. Так, на сегодняшний день асинхронные функции полностью поддерживают все основные браузеры.
Все основные браузеры поддерживают асинхронные функции (caniuse.com)
Такой уровень поддержки означает, например, что код, использующий async/await, не нужно транспилировать. Кроме того, это облегчает отладку, что, пожалуй, даже более важно, чем отсутствие необходимости в транспиляции.
На следующем рисунке показан процесс отладки асинхронной функции. Здесь, при установке точки останова на первой инструкции функции и при выполнении команды Step Over, когда отладчик доходит до строки, в которой использовано ключевое слово await
, можно заметить, как отладчик ненадолго приостанавливается, ожидая окончания работы функции bookModel.fetchAll()
, а затем переходит к строке, где вызывается команда .filter()
! Такой отладочный процесс выглядит куда проще, чем отладка промисов. Тут, при отладке аналогичного кода, пришлось бы устанавливать ещё одну точку останова в строке .filter()
.
Отладка асинхронной функции. Отладчик дождётся выполнения await-строки и перейдёт на следующую строку после завершения операции
Ещё одна сильная сторона рассматриваемого механизма, которая менее очевидна чем то, что мы уже рассмотрели, заключается в наличии здесь ключевого слова async
. В нашем случае его использование гарантирует то, что значение, возвращаемое функцией getBooksByAuthorWithAwait()
будет промисом. В результате в коде, вызывающем эту функцию, можно безопасно воспользоваться конструкцией getBooksByAuthorWithAwait().then(...)
или await getBooksByAuthorWithAwait()
. Поразмыслите над следующим примером (учтите, что так делать не рекомендуется):
getBooksByAuthorWithPromise(authorId) {
if (!authorId) {
return null;
}
return bookModel.fetchAll()
.then(books => books.filter(b => b.authorId === authorId));
}
}
Здесь функция getBooksByAuthorWithPromise()
может, если всё нормально, вернуть промис, или, если что-то пошло не так — null
. В результате, если произошла ошибка, здесь нельзя безопасно вызвать .then()
. При объявлении функций с использованием ключевого слова async
ошибки подобного рода невозможны.
О неправильном восприятии async/await
В некоторых публикациях конструкцию async/await сравнивают с промисами и говорят о том, что она представляет собой новое поколении эволюции асинхронного программирования на JavaScript. С этим я, при всём уважении к авторам таких публикаций, позволю себе не согласиться. Async/await — это улучшение, но это — не более чем «синтаксический сахар», появление которого не ведёт к полному изменению стиля программирования.
В сущности, асинхронные функции — это промисы. Перед тем, как программист сможет правильно использовать конструкцию async/await, он должен хорошо изучить промисы. Кроме того, в большинстве случаев, работая с асинхронными функциями, нужно использовать и промисы.
Взгляните на функции getBooksByAuthorWithAwait()
и getBooksByAuthorWithPromises()
из вышеприведённого примера. Обратите внимание на то, что они идентичны не только в плане функционала. У них ещё и совершенно одинаковые интерфейсы.
Всё это значит, что, если вызвать напрямую функцию getBooksByAuthorWithAwait()
, она вернёт промис.
На самом деле, суть проблемы, о которой мы тут говорим, заключается в неправильном восприятии новой конструкции, когда создаётся обманчивое ощущение того, что синхронную функцию можно конвертировать в асинхронную благодаря простому использованию ключевых слов async
и await
и ни о чём больше не задумываться.
Подводные камни async/await
Поговорим о наиболее распространённых ошибках, которые можно сделать, пользуясь async/await. В частности — о нерациональном использовании последовательных вызовов асинхронных функций.
Хотя ключевое слово await
может сделать код похожим на синхронный, пользуясь им, стоит помнить о том, что код это асинхронный, а значит, надо очень внимательно относиться к последовательным вызовом асинхронных функций.
async getBooksAndAuthor(authorId) {
const books = await bookModel.fetchAll();
const author = await authorModel.fetch(authorId);
return {
author,
books: books.filter(book => book.authorId === authorId),
};
}
Этот код, с точки зрения логики, кажется правильным. Однако тут имеется серьёзная проблема. Вот как он работает.
- Система вызывает
await bookModel.fetchAll()
и ждёт завершения команды.fetchAll()
. - После получения результата от
bookModel.fetchAll()
будет выполнен вызовawait authorModel.fetch(authorId)
.
Обратите внимание на то, что вызов authorModel.fetch(authorId)
не зависит от результатов вызова bookModel.fetchAll()
, и, на самом деле, эти две команды можно выполнять параллельно. Однако использование await
приводит к тому, что два этих вызова выполняются последовательно. Общее время последовательного выполнения этих двух команд будет больше, чем время их параллельного выполнения.
Вот правильный подход к написанию такого кода:
async getBooksAndAuthor(authorId) {
const bookPromise = bookModel.fetchAll();
const authorPromise = authorModel.fetch(authorId);
const book = await bookPromise;
const author = await authorPromise;
return {
author,
books: books.filter(book => book.authorId === authorId),
};
}
Рассмотрим ещё один пример неправильного использования асинхронных функций. Тут всё ещё хуже, чем в предыдущем примере. Как видите, для того, чтобы асинхронно загрузить список неких элементов, нам надо полагаться на возможности промисов.
async getAuthors(authorIds) {
// Неправильный подход, вызовы будут выполнены последовательно
// const authors = _.map(
// authorIds,
// id => await authorModel.fetch(id));
// Правильный подход
const promises = _.map(authorIds, id => authorModel.fetch(id));
const authors = await Promise.all(promises);
}
Если в двух словах, то, для того, чтобы грамотно пользоваться асинхронными функциями, нужно, как и во времена, когда этой возможности не было, сначала подумать об асинхронном выполнении операций, а потом уже писать код с применением await
. В сложных случаях, вероятно, легче будет просто напрямую использовать промисы.
Обработка ошибок
При использовании промисов выполнение асинхронного кода может завершиться либо так, как ожидается — тогда говорят об успешном разрешении промиса, либо с ошибкой — тогда говорят о том, что промис отклонён. Это даёт нам возможность использовать, соответственно, .then()
и .catch()
. Однако, обработка ошибок при использовании механизма async/await может оказаться непростым делом.
▍Конструкция try/catch
Стандартным способом для обработки ошибок при использовании async/await является конструкция try/catch. Я рекомендую пользоваться именно этим подходом. При выполнении await-вызова значение, выдаваемое при отклонении промиса, представляется в виде исключения. Вот пример:
class BookModel {
fetchAll() {
return new Promise((resolve, reject) => {
window.setTimeout(() => { reject({'error': 400}) }, 1000);
});
}
}
// async/await
async getBooksByAuthorWithAwait(authorId) {
try {
const books = await bookModel.fetchAll();
} catch (error) {
console.log(error); // { "error": 400 }
}
Ошибка, перехваченная в блоке catch
— это как раз и есть значение, получающееся при отклонении промиса. После перехвата исключения мы можем применить несколько подходов для работы с ним:
- Можно обработать исключение и вернуть нормальное значение. Если не использовать выражение
return
в блокеcatch
для возврата того, что ожидается после выполнения асинхронной функции, это будет эквивалентно использованию командыreturn undefined
;. - Можно просто передать ошибку в место вызова кода, который дал сбой, и позволить обработать её там. Можно выбросить ошибку напрямую, воспользовавшись командой наподобие
throw error;
, что позволит использовать функциюasync getBooksByAuthorWithAwait()
в цепочке промисов. То есть, вызывать её можно будет, пользуясь конструкциейgetBooksByAuthorWithAwait().then(...).catch(error => ...)
. Кроме того, можно обернуть ошибку в объектError
, что может выглядеть какthrow new Error(error)
. Это позволит, например, при выводе сведений об ошибке в консоль, просмотреть полный стек вызовов. - Ошибку можно представить в виде отклонённого промиса, выглядит это как
return Promise.reject(error)
. В данном случае это эквивалентно командеthrow error
, делать так не рекомендуется.
Вот преимущества применения конструкции try/catch:
- Подобные средства обработки ошибок существуют в программировании уже очень давно, они просты и понятны. Скажем, если у вас есть опыт программирования на других языках, вроде C++ или Java, то вы без проблем поймёте устройство try/catch в JavaScript.
- В один блок try/catch можно помещать несколько await-вызовов, что позволяет обрабатывать все ошибки в одном месте в том случае, если нет необходимости раздельно обрабатывать ошибки на каждом шаге выполнения кода.
Надо отметить, что в механизме try/catch есть один недостаток. Так как try/catch перехватывает любые исключения, возникающие в блоке try
, в обработчик catch
попадут и те исключения, которые не относятся к промисам. Взгляните на этот пример.
class BookModel {
fetchAll() {
cb(); // обратите внимание на то, что функция `cb` не определена, что приведёт к исключению
return fetch('/books');
}
}
try {
bookModel.fetchAll();
} catch(error) {
console.log(error); // Тут будет выдано сообщение об ошибке "cb is not defined"
}
Если выполнить этот код, можно увидеть в консоли сообщение об ошибке ReferenceError: cb is not defined
. Это сообщение выведено командой console.log()
из блока catch
, а не самим JavaScript. В некоторых случаях такие ошибки приводят к тяжёлым последствиям. Например, если вызов bookModel.fetchAll();
запрятан глубоко в серии вызовов функций и один из вызовов «проглотит» ошибку, такую ошибку будет очень сложно обнаружить.
▍Возврат функциями двух значений
Источником вдохновения для следующего способа обработки ошибок в асинхронном коде стал язык Go. Он позволяет асинхронным функциям возвращать и ошибку, и результат. Подробнее об этом можно почитать здесь.
Если в двух словах, то асинхронные функции, при таком подходе, можно использовать так:
[err, user] = await to(UserModel.findById(1));
Лично мне это не нравится, так как этот способ обработки ошибок привносит в JavaScript стиль программирования на Go, что выглядит неестественно, хотя, в некоторых случаях, это может оказаться весьма полезным.
▍Использование .catch
Последний способ обработки ошибок, о котором мы поговорим, заключается в использовании .catch()
.
Вспомните о том, как работает await
. А именно, использование этот ключевого слова приводит к тому, что система ждёт до тех пор, пока промис не завершит свою работу. Кроме того, вспомните о том, что команда вида promise.catch()
тоже возвращает промис. Всё это говорит о том, что обрабатывать ошибки асинхронных функций можно так:
// books будет равно undefined если произойдёт ошибка,
// так как обработчик catch ничего явно не возвращает
let books = await bookModel.fetchAll()
.catch((error) => { console.log(error); });
Для этого подхода характерны две небольших проблемы:
- Это — смесь промисов и асинхронных функций. Для того чтобы этим пользоваться, надо, как и в других подобных случаях, понимать особенности работы промисов.
- Этот подход не отличается интуитивной понятностью, так как обработка ошибок выполняется в необычном месте.
Итоги
Конструкция async/await, которая появилась в ES7, определённо, является улучшением механизмов асинхронного программирования в JavaScript. Она способна облегчить чтение и отладку кода. Однако, для того, чтобы пользоваться async/await правильно, необходимо глубокое понимание промисов, так как async/await — это всего лишь «синтаксический сахар», в основе которого лежат промисы.
Надеемся, этот материал позволил вам ближе познакомиться с async/await, и то, что вы тут узнали, убережёт вас от некоторых распространённых ошибок, возникающих при использовании этой конструкции.
Уважаемые читатели! Пользуетесь ли вы конструкцией async/await в JavaScript? Если да — просим рассказать о том, как вы обрабатываете ошибки в асинхронном коде.
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`, если необходимо выполнить несколько задач параллельно.
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
.
Оригинал
А с какими проблемами в асинхронном программировании сталкивались вы?
This article is intended to suggest a better way to handle errors when using async/await syntax. Prior knowledge of how promises work is important.
From Callback Hell to Promises
Callback Hell, also known as Pyramid of Doom, is an anti-pattern seen in code of programmers who are not wise in the ways of asynchronous programming. — Colin Toh
Callback hell makes your code drift to the right instead of downward due to multiple nesting of callback functions.
I wont go into details of what callback hell is, but I’ll give an example of how it looks.
User profile example 1
// Code that reads from left to right
// instead of top to bottom
let user;
let friendsOfUser;
getUser(userId, function(data) {
user = data;
getFriendsOfUser(userId, function(friends) {
friendsOfUser = friends;
getUsersPosts(userId, function(posts) {
showUserProfilePage(user, friendsOfUser, posts, function() {
// Do something here
});
});
});
});
Enter fullscreen mode
Exit fullscreen mode
Promises
Promises were introduced to the Javascript(ES6) language to handle asynchronous operations better without it turning into a callback hell.
The example below use promises to solve callback hell by using multiple chained .then
calls instead of nesting callbacks.
User profile example 2
// A solution with promises
let user;
let friendsOfUser;
getUser().then(data => {
user = data;
return getFriendsOfUser(userId);
}).then(friends => {
friendsOfUser = friends;
return getUsersPosts(userId);
}).then(posts => {
showUserProfilePage(user, friendsOfUser, posts);
}).catch(e => console.log(e));
Enter fullscreen mode
Exit fullscreen mode
The solution with promise looks cleaner and more readable.
Promises with with async/await
Async/await is a special syntax to work with promises in a more concise way.
Adding async before any function
turns the function into a promise.
All
async
functions return promises.
Example
// Arithmetic addition function
async function add(a, b) {
return a + b;
}
// Usage:
add(1, 3).then(result => console.log(result));
// Prints: 4
Enter fullscreen mode
Exit fullscreen mode
Making the User profile example 2
look even better using async/await
User profile example 3
async function userProfile() {
let user = await getUser();
let friendsOfUser = await getFriendsOfUser(userId);
let posts = await getUsersPosts(userId);
showUserProfilePage(user, friendsOfUser, posts);
}
Enter fullscreen mode
Exit fullscreen mode
Wait! there’s a problem
If theres a promise rejection in any of the request in User profile example 3
, Unhandled promise rejection
exception will be thrown.
Before now Promise rejections didn’t throw errors. Promises with unhandled rejections used to fail silently, which could make debugging a nightmare.
Thank goodness promises now throws when rejected.
-
Google Chrome throws:
VM664:1 Uncaught (in promise) Error
-
Node will throw something like:
(node:4796) UnhandledPromiseRejectionWarning: Unhandled promise rejection (r ejection id: 1): Error: spawn cmd ENOENT
[1] (node:4796) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
No promise should be left uncaught. — Javascript
Notice the .catch
method in User profile example 2
.
Without the .catch block Javascript will throw Unhandled promise rejection
error when a promise is rejected.
Solving this issue in User profile example 3
is easy. Unhandled promise rejection
error can be prevented by wrapping await operations in a try…catch block:
User profile example 4
async function userProfile() {
try {
let user = await getUser();
let friendsOfUser = await getFriendsOfUser(userId);
let posts = await getUsersPosts(userId);
showUserProfilePage(user, friendsOfUser, posts);
} catch(e) {
console.log(e);
}
}
Enter fullscreen mode
Exit fullscreen mode
Problem solved!
…But error handling could be improved
How do you know with error is from which async request?
We can call a .catch
method on the async requests to handle errors.
User profile example 5
let user = await getUser().catch(e => console.log('Error: ', e.message));
let friendsOfUser = await getFriendsOfUser(userId).catch(e => console.log('Error: ', e.message));
let posts = await getUsersPosts(userId).catch(e => console.log('Error: ', e.message));
showUserProfilePage(user, friendsOfUser, posts);
Enter fullscreen mode
Exit fullscreen mode
The solution above will handle individual errors from the requests, but its a mix of patterns. There should be a cleaner way to use async/await without using .catch
method (Well, you could if you don’t mind).
Here’s my solution to a better async/await error handling
User profile example 6
/**
* @description ### Returns Go / Lua like responses(data, err)
* when used with await
*
* - Example response [ data, undefined ]
* - Example response [ undefined, Error ]
*
*
* When used with Promise.all([req1, req2, req3])
* - Example response [ [data1, data2, data3], undefined ]
* - Example response [ undefined, Error ]
*
*
* When used with Promise.race([req1, req2, req3])
* - Example response [ data, undefined ]
* - Example response [ undefined, Error ]
*
* @param {Promise} promise
* @returns {Promise} [ data, undefined ]
* @returns {Promise} [ undefined, Error ]
*/
const handle = (promise) => {
return promise
.then(data => ([data, undefined]))
.catch(error => Promise.resolve([undefined, error]));
}
async function userProfile() {
let [user, userErr] = await handle(getUser());
if(userErr) throw new Error('Could not fetch user details');
let [friendsOfUser, friendErr] = await handle(
getFriendsOfUser(userId)
);
if(friendErr) throw new Error('Could not fetch user's friends');
let [posts, postErr] = await handle(getUsersPosts(userId));
if(postErr) throw new Error('Could not fetch user's posts');
showUserProfilePage(user, friendsOfUser, posts);
}
Enter fullscreen mode
Exit fullscreen mode
Using the handle
utility function, we are able to avoid Unhandled promise rejection
error and also handle error granularly.
Explanation
The handle
utility function takes a promise as an argument and always resolves it, returning an array with [data|undefined, Error|undefined]
.
- If the promise passed to the
handle
function resolves it returns[data, undefined]
; - If it was rejected, the
handle
function still resolves it and returns[undefined, Error]
Similar solutions
- Easier Error Handling Using Async/Await — Jesse Warden
- NPM Package — await-to-js
Conclusion
Async/await has a clean syntax, but you still have to handle thrown exceptions in async functions.
Handling error with .catch
in promise .then
chain can be difficult unless you implement custom error classes.
Using the handle
utility function, we are able to avoid Unhandled promise rejection
error and also handle error granularly.