Что такое Async/Await?

Async/await — это концепция, возникшая в версии Javascript 2017 года, которая позволяет синхронно (по порядку) учитывать асинхронные действия (когда в программе одновременно происходит одно или несколько действий). В Javascript асинхронное поведение обычно возникает в форме запросов API или промисов, созданных вручную. Большинство других функций в Javascript являются синхронными.

Использование async/await расширяет функцию Promise, которая была выпущена в Javascript в 2015 году. Промисы — это решение для вложенных функций обратного вызова, с помощью которых асинхронное поведение обрабатывалось до того, как были выпущены промисы. Обратные вызовы (функция, передаваемая другой функции в качестве аргумента) создавали проблему, часто называемую адом обратных вызовов, когда функции вкладывались друг в друга до неразборчивости. Промисы позволяли связывать функции с методом .then(), чтобы сделать программу более читабельной. Async/await делает шаг вперед, чтобы уйти от вложенности и цепочки к синтаксису, который больше похож на синхронный Javascript, к которому привыкло большинство людей.

Async/await был разработан как «синтаксический сахар» поверх промисов и может использоваться только с промисами, которые были созданы ранее, или для обработки ответов HTTP (которые обычно являются промисами под капотом).

Ключевое слово async помечает функцию как содержащую асинхронный контент (промис-разрешение или HTTP-запрос). Ключевое слово await указывает, на какой строке программа должна остановиться, чтобы дождаться разрешения промиса.

Вот простой пример:

const promiseExample = new Promise((resolve, reject) => {
  const randomBoolean = Math.random() < 0.5;
  if (randomBoolean) {
    console.log(randomBoolean);
    resolve('Boolean was true!')
  } else {
    console.log(randomBoolean);
    reject('Boolean was false!')
  }
})

async function asyncExample() {
  try {
    const promiseOutcome = await promiseExample;
    console.log('SUCCESS:', promiseOutcome);
  } catch(err) {
    console.log('ERROR:', err);
  }
}

asyncExample();

Здесь у нас есть переменная promiseExample, которая принимает результат промиса. В этом случае переменная randomBoolean будет случайным образом разрешаться либо в true, либо в false. Если это true, то Promise успешно кодируется как resolve. Если это false, то обещание будет reject или не будет выполнено.

Функция asyncExample помечена ключевым словом async, которое указывает, что она содержит асинхронный контент. Оттуда ключевое слово await сообщает программе, на какой строке ждать, пока она не получит значение (или не возникнет ошибка).

Если асинхронное действие происходит без надлежащей обработки, вы можете обнаружить, что переменные, зависящие от результата этого асинхронного действия, являются undefined. Это происходит, когда программа пытается прочитать и использовать эти переменные перед им было присвоено значение структурой Promise. По этой причине важно правильно обрабатывать промисы с обратными вызовами, цепочкой .then() или с использованием async/await .

Идите вперед и запустите приведенный выше код несколько раз, чтобы увидеть, что вы получите!

Когда использовать Async/Await

Синтаксис async/await используется ТОЛЬКО в сочетании с промисами. Это оптимизация, разработанная специально для того, чтобы сделать асинхронные действия, происходящие при использовании промисов, более читабельными. Async/await позволяет вам читать код по мере его выполнения, строка за строкой, вместо того, чтобы прыгать по коду, чтобы увидеть, что произойдет дальше по порядку.

Преимущества использования Async/Await

Async/await позволяет сделать ваш код более читабельным. Он отходит от вложенных обратных вызовов или цепочек .then() методов, которые могут сделать ваш код беспорядочным и трудным для отладки.

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

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

Обработка ошибок с async/await выглядит немного иначе, чем если бы вы использовали только структуру Promise. Помните, что с промисами вы будете обрабатывать ошибки с .catch() следующим образом:

doSomething()
  .then(() => <DO SOMETHING>)  
  .then(() => <DO SOMETHING ELSE>)
  .catch((err) => <HANDLE ERROR HERE>)

С async/await ошибки обрабатываются блоком try/catch следующим образом:

async function doSomething() {
  try {
    <PUT YOUR CODE HERE ALONG WITH THE AWAIT LINE(s)>
  } catch(err) {
    <HANDLE ERROR HERE>
  }
}

Этот синтаксис попытается запустить ваш код в части try блока, и если ЛЮБОЕ из промисов, на которые ссылается там await, потерпит неудачу, то они будут перехвачены в части catch блока.

Не забудьте блок try/catch! Если вы это сделаете, и ваш Promise rejects, тогда ваш код выдаст ошибку Unhandled Promise, поскольку вы не предоставили выход сообщению об ошибке.

Пример

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

function generateRandomNum() {
  const randomNum = Math.floor(Math.random() * 11);
  if (randomNum === 4) {
    return 'FOUR';
  } else {
   return randomNum; 
  }
}

Вы можете заметить, что эта функция содержит поворот, чтобы сделать вещи интересными. Он вернет случайное число, ЕСЛИ ТОЛЬКО это число не окажется 4, и в этом случае он вернет строку FOUR .

Затем мы можем создать функцию, которая возвращает результат Promise, который проверяет тип данных числа, сгенерированного функцией generateRandomNum(). Эта функция принимает один аргумент, operandPosition, который поможет нам определить, является ли первый или второй операнд (или число, которое нужно сложить вместе) результатом проблемы:

function generateNumPromise(operandPosition) {
  const randomNum = generateRandomNum();
  let result = new Promise((resolve, reject) => {
    if (typeof randomNum === 'number') {
     resolve(randomNum);
    } else {
     reject(`There was a problem! The data type generated for the ${operandPosition} operand was not a number.`);
    }
  })
  console.log(`The ${operandPosition} number is ${randomNum}.`)
  return result;
}

Эта функция содержит обещание, которое будет resolve (считаться успешным), если randomNum является числовым типом данных, или будет reject (считаться ошибочным), если randomNum является строкой. В любом случае console.log будет выводить значение каждого операнда каждый раз, когда программа запускается для наглядности. Затем результат Promise будет возвращен как result .

Оттуда мы будем использовать функцию async для await разрешения каждого промиса, сгенерированного двойным вызовом generateNumPromise(). Помните, что async/await используется только тогда, когда есть промис для ожидания, поэтому он вам никогда не понадобится, если вы не добавите промис в свой код или не сделаете HTTP-запрос, поскольку они обычно возвращают промисы:

async function addNumbers() {
  try {
    const firstNum = await generateNumPromise('first');
    const secondNum = await generateNumPromise('second');
    console.log(`The sum of firstNum and secondNum = ${firstNum + secondNum}.`);
  } catch (err) {
    console.error(err);
  }
}

addNumbers();

Вот пара запусков программы:

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

На втором снимке экрана мы видим, что вместо числа была сгенерирована строка, которая выдает ошибку, вызывая сообщение reject в блоке try/catch.

Ключевые выводы

  • async/await всегда нужно использовать с промисами (помните, метод Node fetch и другие инструменты запросов API возвращают промисы)
  • не забудьте использовать блок try/catch для обработки ошибок, чтобы не столкнуться с проблемами с неразрешенными промисами в коде.
  • await можно использовать только в функции с меткой async

Дополнительные ресурсы

Синхронный и асинхронный в Javascript

Видео Javascript Promises vs Async Await ОБЪЯСНЕНИЕ (через 5 минут)