Механизм async/await , представленный ES7, является фантастическим улучшением асинхронного программирования с использованием JavaScript. Он предоставил возможность использовать код, написанный в синхронном стиле, для асинхронного доступа к ресурсам, при котором не блокируется основной поток. Однако, применение этого механизма — задача непростая. В этой статье мы рассмотрим async / wait с разных точек зрения и покажем, как использовать его правильно и эффективно.
Что хорошего в async/await
Важнейшим преимуществом async/await является синхронный стиль программирования. Давайте посмотрим на следующий пример:
Очевидно, что вариант сasync/await читать и понимать проще, чем версию того же кода с промисами (promise). Если убрать ключевое слово await, то код станет выглядеть так, как будто написан на любом другом синхронном языке программированя, такие как Python.
И еще из приятного — это не только читаемость: async/await по умолчанию поддерживается всеми основными современными браузерами.
Встроенная поддержка означает, что вам не нужно транспилировать код. Что еще более важно, это облегчает отладку. Если установить контрольную точку (breakpoint) в точке входа функции, то после выполнения строки сawaitотладчик (debugger) ненадолго подвиснет на врем, которое требуетсяbookModel.fetchAll() для выполнения своей работы, а затем перейдет к следующей строке .filter Это намного проще, чем в ситуации с промисами, в которой вам пришлось бы настраивать другую контрольную точку в строке с .filter
Другим менее очевидным преимуществом моно назвать ключевое словоasync. Оно свидетельствует о том, что функцияgetBooksByAuthorWithAwait() вернет значение, которое гарантированно будет промисом, поэтому можно безопасно вызывать методыgetBooksByAuthorWithAwait().then(...) или await getBooksByAuthorWithAwait() . Взгляните внимательно на этот случай (но не пишите так! Это плохая практика):
В приведенном выше примере кода getBooksByAuthorWithPromise может вернуть промис (нормальное поведение) или null (исключение), и в этом случае не удастся безопасно вызвать метод .then(). При использовании async такой кейс не возможен в принципе.
Async / wait может ввести в заблуждение
Автор некоторых статей сравнивают async / await с промисами и утверждают, что это следующее поколение в эволюции асинхронного программирования JavaScript, — при всем моем уважении, с этой точкой зрения я не согласен. Async / await — это улучшение, но в то же время не стоит считать его чем то более значительным, чем синтаксический сахар, потому что стиль программирования он кардинально не меняет.
По сути, асинхронные функции по-прежнему остаются промисами. Вы должны понять, как работают промисы, прежде чем сможете научиться правильно использовать асинхронные функции , и что еще хуже, большую часть времени вы должны использовать промисы вместе с асинхронными функциями.
Рассмотрим функции getBooksByAuthorWithAwait() и getBooksByAuthorWithPromises() в приведенном выше примере. Обратите внимание, что они не только идентичны функционально, но и имеют точно такой же интерфейс!
Это означает, что getBooksByAuthorWithAwait() вернет промис, если вы вызовете эту функцию напрямую.
Ну, это не обязательно плохо. Только название await может вызвать мысль: «О, отлично, так можно преобразовать асинхронные функции в синхронные функции»,- что на самом деле неверно.
Ловушки Async/await
Итак, какие ошибки разработчик может допустить при использовании async/await? Вот некоторые наиболее общие.
Слишком много последовательностей
Благодаряawait может создастся впечатление, что код будет исполняться последовательно, имейте в виду, что он все еще асинхронный, и нужно быть осторожными, чтобы не нагромождать слишком много последовательностей.
Этот код выглядит правильно с точки зрения логики. Однако работать он будет некорректно.
- await bookModel.fetchAll() будет ждать ответа от функции fetchAll().
- Затем будет вызван метод await authorModel.fetch(authorId).
Обратите внимание, что authorModel.fetch(authorId) не зависит от результата bookModel.fetchAll(), и на самом деле их можно вызывать параллельно! Однако, используя await, эти два вызова становятся последовательными, и общее время выполнения будет намного больше, чем при параллельном варианте выполнения функций.
Вот правильный способ:
Или того хуже, вдруг вам захочется получить список книг одна за другой, тогда вам придется прибегнуть к промисам:
Короче говоря, вам все равно нужно предусмотреть асинхронный порядок исполнения кода, а затем попытаться написать код последовательно с await. В сложных случаях лучшим вариантом будет использовать промисы напрямую.
Обработка ошибок
При использовании промисов функция async может возвращать два значения: разрешенное значение и отклоненное значение. И мы можем использовать .then() для обычного случая и .catch() для случая с ошибкой. Однако обработвать ошибки в случае с async/await — дело сложное.
try…catch
Самый стандартный способ, его же я обычно рекомендую — использовать конструкцию try...catch. При ожидании вызова, то есть в случае с await,любое отклоненное значение будет выброшено как исключение. Вот пример:
Error в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:
- Простой, традиционный способ. Если у вас есть опыт работы с другими языками, такими как Java или C ++, вам не составит труда понять данную концепцию.
- Дает возможность поместить несколько вызовов awaitв один блок try...catch для обработки ошибок в одном месте, если обработка ошибок на каждом шаге не требуется.
В этом подходе есть и один недостаток. Так как try...catch поймает любое исключение в блоке, то будут выброшены ошибки, которые в обычных случаях промисами не отлавливаются. Для того, чтобы понять эту идею, взгляните на пример:
Запустите этот код и вы получите ошибку ReferenceError: cb is not definedв консоли, черного цвета. Ошибка выводилась с помощью console.log(),но не самим JavaScript. Иногда это может быть фатальным: если BookModelзаключен глубоко в ряд вызовов функций, и один из вызовов проглатывает ошибку, тогда будет очень сложно найти неопределенную ошибку, подобную этой.
Функция возвращает оба значения
Другой способ обработки ошибок практикуется в языке Go. Он позволяет async-функции возвращать как ошибку, так и результат. За более подробным описанием направляю вас в эту статью.
Короче говоря, вы можете использовать async-функцию следующим образом:
[err, user] = await to(UserModel.findById(1));
Лично мне не нравится этот подход, поскольку он привносит стиль Go в JavaScript, который кажется неестественным, но в некоторых случаях это может оказаться весьма полезным.
Использование .catch
И последний способ вызова ошибок, которым мы поделимся здесь, — продолжить использование .catch().
Вспомните функциональность await: эта функция будет ждать, пока промис завершит свою работу. Также, пожалуйста, помните, что promise.catch()тоже вернет промис! Поэтому мы можем написать обработку ошибок следующим образом:
В этом подходе есть две незначительные проблемы:
- Это смесь промисов и асинхронных функций. Вы все еще должны понимать, каков механизм работы промисов, чтобы суметь прочитать этот код.
- Обработка ошибок идет перед основным кодом, что идет вразрез с интуицией.
Вывод
Ключевые слова async/await, введенные ES7, безусловно, значительно упростили асинхронное программирование JavaScript. Это помогает делать код более легким для чтения и отладки. Однако, чтобы правильно использовать их, нужно полностью понимать промисы, так как они представляют собой всего лишь синтаксический сахар, в основе которого лежат всё те же промисы.
Читайте нас в телеграмме, vk
Перевод статьи Charlee Li: JavaScript async/await: The Good Part, Pitfalls and How to Use