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

Хорошая часть 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));
}

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

И золотая середина - это не только удобочитаемость. async/await имеет встроенную поддержку браузера. На сегодняшний день все основные браузеры полностью поддерживают асинхронные функции.

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

Еще одно менее очевидное преимущество - ключевое слово 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 сравнивается с Promise и утверждается, что это следующее поколение в эволюции асинхронного программирования JavaScript, с чем я с уважением не согласен. Async / await ЯВЛЯЕТСЯ улучшением, но это не более чем синтаксический сахар, который не изменит полностью наш стиль программирования.

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

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

Это означает, что getBooksByAuthorWithAwait() вернет обещание, если вы вызовете его напрямую.

Что ж, это не обязательно плохо. Только имя 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),
  };
}

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

  1. await bookModel.fetchAll() будет ждать, пока не вернется fetchAll().
  2. Тогда будет вызван 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) {
  // WRONG, this will cause sequential calls
  // const authors = _.map(
  //   authorIds,
  //   id => await authorModel.fetch(id));
// CORRECT
  const promises = _.map(authorIds, id => authorModel.fetch(id));
  const authors = await Promise.all(promises);
}

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

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

С обещаниями асинхронная функция имеет два возможных возвращаемых значения: разрешенное значение и отклоненное значение. И мы можем использовать .then() для нормального случая и .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 }
}

Ошибка catched - это в точности отклоненное значение. После того, как мы перехватили исключение, у нас есть несколько способов справиться с ним:

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

class BookModel {
  fetchAll() {
    cb();    // note `cb` is undefined and will result an exception
    return fetch('/books');
  }
}
try {
  bookModel.fetchAll();
} catch(error) {
  console.log(error);  // This will print "cb is not defined"
}

Запустите этот код, и вы получите сообщение об ошибке ReferenceError: cb is not defined в консоли черного цвета. Ошибка была выдана console.log(), но не самим JavaScript. Иногда это может быть фатальным: если BookModel глубоко заключен в серию вызовов функций и один из вызовов принимает ошибку, тогда будет чрезвычайно сложно найти неопределенную ошибку, подобную этой.

Заставить функции возвращать оба значения

Другой способ обработки ошибок основан на языке Go. Это позволяет асинхронной функции возвращать как ошибку, так и результат. Подробности см. В этом сообщении в блоге:



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

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

Лично мне не нравится этот подход, поскольку он привносит стиль Go в JavaScript, что кажется неестественным, но в некоторых случаях это может быть весьма полезно.

Использование .catch

Последний подход, который мы здесь представим, - это продолжить использование .catch().

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

// books === undefined if error happens,
// since nothing returned in the catch statement
let books = await bookModel.fetchAll()
  .catch((error) => { console.log(error); });

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

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

Заключение

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

Надеюсь, этот пост даст вам некоторое представление о самих async/await и поможет предотвратить некоторые типичные ошибки. Спасибо за чтение, и, пожалуйста, хлопайте мне в ладоши, если вам понравился этот пост.