async/await в JavaScript: преимущества и подводные камни

Конструкция async/await сделала большой вклад в асинхронное JS-программирование: можно использовать стиль синхронного кода для асинхронного.

Плюсы async/await

Существенным плюсом async/await является возможность использования синхронного стиля программирования. Пример:

// async/await
async getBooksByAuthorWithAwait(authorId) {
  const books = await bookModel.fetchAll();
  return books.filter(b => b.authorId === authorId);
}
// promise
getBooksByAuthorWithPromise(authorId) {
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
}

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

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

async/await

Еще одним, но, пожалуй, менее очевидным преимуществом является async. Он объявляет, что функция getBooksByAuthor(), которая возвращает значение wait(), точно является promise. Поэтому можно безопасно вызывать getBooksByAuthorWithAwait().then.(..) или await getBooksByAuthorWithAwait().

getBooksByAuthorWithPromise(authorId) {
  if (!authorId) {
    return null;
  }
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
}

В приведенном коде getBooksByAuthorWithPromise() может возвращать promise (обычный случай) или null (частный случай), но тогда нельзя будет безопасно вызвать .then().

async/await способен ввести в заблуждение

Иногда сравнение с promise сопровождается утверждением, будто это эволюция асинхронного JS-программирования, что довольно спорно. async/await – лишь “приправа” для вашего кода, его небольшой апдейт, и никак не изменит ваш стиль программирования полностью.

По сути, promise – это как раз те самые асинхронные функции. Чтобы их юзать, нужно сперва хорошо разобраться с promise. Его все же придется использовать чаще.

Рассмотрим функции getBooksByAuthorWithwait() и getBooksByAuthorWithPromises() в приведенном выше примере. Помимо функциональной идентичности, они также имеют одинаковый интерфейс. Таким образом, getBooksByAuthorWithAwait() вернет promise, если вызвать его напрямую.

Подводные камни в 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().
  • После этого вызывается 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),
  };
}

Если вы хотите получить список элементов, необходимо полагаться на promise:

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. В некоторых случаях использование promise будет более оправданным решением.

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

С помощью promises асинхронная функция имеет два возможных возвращаемых значения: resolved value (разрешенное) и rejected value (отклоненное). Можно использовать .then() для обычного случая и .catch() для частного. Но, к сожалению, обработка ошибок async/await – довольно сложный процесс. Рассмотрим популярные примеры.

Try...catch

В основном применение блока try...catch влечет за собой принятие rejected value как resolved value. Пример:

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 }
}

Преимущества использования try...catch:

  • Простота. Если вы умеете работать с такими языками, как Java или C++, у вас не будет никаких трудностей.
  • Чтобы обработать ошибки в конкретной части программы, в 1 блок try...catch можно обернуть несколько await.

Существует также один недостаток в этом подходе. try...catch будет находить каждое исключение в блоке, которые обычно не определяется с помощью promise. Пример:

class BookModel {
  fetchAll() {
    cb();    // "cb" не определен, что приведет к исключению
    return fetch('/books');
  }
}
try {
  bookModel.fetchAll();
} catch(error) {
  console.log(error);  // Выведет "cb is not defined"
}

При запуске данного кода выведется ошибка: ReferenceError: cb не определен. Так происходит из-за того, что BookModel вызывается несколько раз, и один вызов проглатывает "Error".

Функция возвращает оба значения

Можно также обработать ошибки с помощью языка Go. Так, будут возвращены и результат, и ошибка. Пример использования функции:

[err, user] = await to(UserModel.findById(1));

.catch

Вспомним функционал await: он будет ждать promise, чтобы завершить работу. Также помним, что promise.catch() возвращает promise. Зная это, вполне можно написать такую обработку:

// Если произошла ошибка, books === undefined 
// поскольку ничего не вернулось в Catch
let books = await bookModel.fetchAll()
  .catch((error) => { console.log(error); });

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

  • Это смесь promise и асинхрон-функций.
  • Обработка ошибок происходит перед телом программы, что интуитивно не понятно.

Подведем итоги

Конструкция async/await неплохо улучшает асинхронное JS-программирование: анализ и отладка становятся проще. Но для корректного использования этих инструментов необходимо полностью понимать promise, т. к. он лежит в их основе

Оригинал

Другие материалы по теме:

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ