Найти тему
Nuances of programming

JavaScript async/await: что хорошего, в чём опасность и как применять?

Оглавление

Механизм async/await , представленный ES7, является фантастическим улучшением асинхронного программирования с использованием JavaScript. Он предоставил возможность использовать код, написанный в синхронном стиле, для асинхронного доступа к ресурсам, при котором не блокируется основной поток. Однако, применение этого механизма — задача непростая. В этой статье мы рассмотрим async / wait с разных точек зрения и покажем, как использовать его правильно и эффективно.

Что хорошего в async/await

Важнейшим преимуществом async/await является синхронный стиль программирования. Давайте посмотрим на следующий пример:

-2

Очевидно, что вариант сasync/await читать и понимать проще, чем версию того же кода с промисами (promise). Если убрать ключевое слово await, то код станет выглядеть так, как будто написан на любом другом синхронном языке программированя, такие как Python.

И еще из приятного — это не только читаемость: async/await по умолчанию поддерживается всеми основными современными браузерами.

Нативная поддержка асинхронных функций популярными браузерами. Источник — https://caniuse.com/
Нативная поддержка асинхронных функций популярными браузерами. Источник — https://caniuse.com/

Встроенная поддержка означает, что вам не нужно транспилировать код. Что еще более важно, это облегчает отладку. Если установить контрольную точку (breakpoint) в точке входа функции, то после выполнения строки сawaitотладчик (debugger) ненадолго подвиснет на врем, которое требуетсяbookModel.fetchAll() для выполнения своей работы, а затем перейдет к следующей строке .filter Это намного проще, чем в ситуации с промисами, в которой вам пришлось бы настраивать другую контрольную точку в строке с .filter

Отладка функции async. Отладчик сделает остановку на строчке await, а когда дождется выполнения функции, двинется к следующей
Отладка функции async. Отладчик сделает остановку на строчке await, а когда дождется выполнения функции, двинется к следующей

Другим менее очевидным преимуществом моно назвать ключевое словоasync. Оно свидетельствует о том, что функцияgetBooksByAuthorWithAwait() вернет значение, которое гарантированно будет промисом, поэтому можно безопасно вызывать методыgetBooksByAuthorWithAwait().then(...) или await getBooksByAuthorWithAwait() . Взгляните внимательно на этот случай (но не пишите так! Это плохая практика):

-5

В приведенном выше примере кода getBooksByAuthorWithPromise может вернуть промис (нормальное поведение) или null (исключение), и в этом случае не удастся безопасно вызвать метод .then(). При использовании async такой кейс не возможен в принципе.

Async / wait может ввести в заблуждение

Автор некоторых статей сравнивают async / await с промисами и утверждают, что это следующее поколение в эволюции асинхронного программирования JavaScript, — при всем моем уважении, с этой точкой зрения я не согласен. Async / await — это улучшение, но в то же время не стоит считать его чем то более значительным, чем синтаксический сахар, потому что стиль программирования он кардинально не меняет.

По сути, асинхронные функции по-прежнему остаются промисами. Вы должны понять, как работают промисы, прежде чем сможете научиться правильно использовать асинхронные функции , и что еще хуже, большую часть времени вы должны использовать промисы вместе с асинхронными функциями.

Рассмотрим функции getBooksByAuthorWithAwait() и getBooksByAuthorWithPromises() в приведенном выше примере. Обратите внимание, что они не только идентичны функционально, но и имеют точно такой же интерфейс!

Это означает, что getBooksByAuthorWithAwait() вернет промис, если вы вызовете эту функцию напрямую.

Ну, это не обязательно плохо. Только название await может вызвать мысль: «О, отлично, так можно преобразовать асинхронные функции в синхронные функции»,- что на самом деле неверно.

Ловушки Async/await

Итак, какие ошибки разработчик может допустить при использовании async/await? Вот некоторые наиболее общие.

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

Благодаряawait может создастся впечатление, что код будет исполняться последовательно, имейте в виду, что он все еще асинхронный, и нужно быть осторожными, чтобы не нагромождать слишком много последовательностей.

-6

Этот код выглядит правильно с точки зрения логики. Однако работать он будет некорректно.

  • await bookModel.fetchAll() будет ждать ответа от функции fetchAll().
  • Затем будет вызван метод await authorModel.fetch(authorId).

Обратите внимание, что authorModel.fetch(authorId) не зависит от результата bookModel.fetchAll(), и на самом деле их можно вызывать параллельно! Однако, используя await, эти два вызова становятся последовательными, и общее время выполнения будет намного больше, чем при параллельном варианте выполнения функций.

Вот правильный способ:

-7

Или того хуже, вдруг вам захочется получить список книг одна за другой, тогда вам придется прибегнуть к промисам:

-8

Короче говоря, вам все равно нужно предусмотреть асинхронный порядок исполнения кода, а затем попытаться написать код последовательно с await. В сложных случаях лучшим вариантом будет использовать промисы напрямую.

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

При использовании промисов функция async может возвращать два значения: разрешенное значение и отклоненное значение. И мы можем использовать .then() для обычного случая и .catch() для случая с ошибкой. Однако обработвать ошибки в случае с async/await — дело сложное.

try…catch

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

-9

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 поймает любое исключение в блоке, то будут выброшены ошибки, которые в обычных случаях промисами не отлавливаются. Для того, чтобы понять эту идею, взгляните на пример:

-10

Запустите этот код и вы получите ошибку 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()тоже вернет промис! Поэтому мы можем написать обработку ошибок следующим образом:

-11

В этом подходе есть две незначительные проблемы:

  • Это смесь промисов и асинхронных функций. Вы все еще должны понимать, каков механизм работы промисов, чтобы суметь прочитать этот код.
  • Обработка ошибок идет перед основным кодом, что идет вразрез с интуицией.

Вывод

Ключевые слова async/await, введенные ES7, безусловно, значительно упростили асинхронное программирование JavaScript. Это помогает делать код более легким для чтения и отладки. Однако, чтобы правильно использовать их, нужно полностью понимать промисы, так как они представляют собой всего лишь синтаксический сахар, в основе которого лежат всё те же промисы.

Читайте нас в телеграмме, vk

Перевод статьи Charlee Li: JavaScript async/await: The Good Part, Pitfalls and How to Use