Понимание цикла событий, обратных вызовов, обещаний и Async/Await в JavaScript

Автор выбрал Фонд помощи по COVID-19 для получения пожертвования в рамках программы Пиши за пожертвования.

Введение

В первые дни интернета веб-сайты часто состояли из статических данных на странице HTML. Но сейчас, когда веб-приложения стали более интерактивными и динамичными, стало все более необходимо выполнять интенсивные операции, такие как выполнение внешних сетевых запросов для получения данных API. Для обработки этих операций в JavaScript разработчик должен использовать техники асинхронного программирования.

Поскольку JavaScript является однопоточным языком программирования с синхронной моделью выполнения, которая обрабатывает одну операцию за другой, он может обрабатывать только один оператор за раз. Однако действие, такое как запрос данных из API, может занять неопределенное количество времени, в зависимости от размера запрашиваемых данных, скорости сетевого соединения и других факторов. Если бы вызовы API выполнялись синхронно, браузер не смог бы обрабатывать никакого пользовательского ввода, такого как прокрутка или нажатие кнопки, пока эта операция не завершится. Это известно как блокирование.

Чтобы предотвратить блокировку, в среде браузера есть множество веб-API, к которым JavaScript может получить доступ, и они асинхронны, что означает, что они могут выполняться параллельно с другими операциями, а не последовательно. Это полезно, потому что это позволяет пользователю продолжать использовать браузер нормально, пока асинхронные операции обрабатываются.

Как разработчик JavaScript вам нужно знать, как работать с асинхронными веб-API и обрабатывать ответ или ошибку этих операций. В этой статье вы узнаете о цикле событий, о первоначальном способе работы с асинхронным поведением через обратные вызовы, о дополнении ECMAScript 2015 обещаниями и о современной практике использования async/await.

Примечание: Эта статья сосредоточена на клиентском JavaScript в среде браузера. Те же концепции обычно верны в среде Node.js, однако Node.js использует собственные API на C++ в отличие от веб-API браузера. Для получения дополнительной информации о асинхронном программировании в Node.js, ознакомьтесь с руководством Как писать асинхронный код в Node.js.

Цикл Событий

Этот раздел объяснит, как JavaScript обрабатывает асинхронный код с помощью цикла событий. Сначала будет показана демонстрация работы цикла событий, а затем будут объяснены два элемента цикла событий: стек и очередь.

JavaScript-код, который не использует асинхронные веб-API, будет выполняться синхронно – один за другим, последовательно. Это демонстрируется этим примером кода, который вызывает три функции, каждая из которых выводит число в консоль:

// Определение трех примеров функций
function first() {
  console.log(1)
}

function second() {
  console.log(2)
}

function third() {
  console.log(3)
}

В этом коде вы определяете три функции, которые выводят числа с помощью console.log().

Затем вызывайте функции:

// Выполнение функций
first()
second()
third()

Вывод будет основан на порядке вызова функций – first(), затем second(), а затем third():

Output
1 2 3

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

Добавьте setTimeout в функцию second, чтобы симулировать асинхронный запрос:

// Определение трех примеров функций, но одна из них содержит асинхронный код
function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

setTimeout принимает два аргумента: функцию, которая будет выполняться асинхронно, и время ожидания перед вызовом этой функции. В этом коде вы обернули console.log в анонимную функцию и передали ее в setTimeout, затем установили функцию на выполнение после 0 миллисекунд.

Теперь вызовите функции, как вы делали ранее:

// Выполните функции
first()
second()
third()

Можете ожидать, что при установке setTimeout в 0 эти три функции по-прежнему будут выполняться последовательно. Но из-за асинхронности функция с задержкой будет напечатана последней:

Output
1 3 2

Независимо от того, установлен ли таймаут на ноль секунд или на пять минут, разницы не будет — вызванный асинхронным кодом console.log выполнится после синхронных функций верхнего уровня. Это происходит потому, что среда выполнения JavaScript, в данном случае браузер, использует концепцию цикла событий для обработки параллельных событий. Поскольку JavaScript может выполнять только одно выражение за раз, ему необходим цикл событий, чтобы быть проинформированным о том, когда выполнить то или иное выражение. Цикл событий управляет этим с помощью концепций стека и очереди.

Стек

Стек, или стек вызовов, хранит состояние того, какая функция в данный момент выполняется. Если вы не знакомы с концепцией стека, вы можете представить его как массив с свойствами “Последний вошел, первый вышел” (LIFO), что означает, что можно добавлять или удалять элементы только с конца стека. JavaScript будет выполнять текущий фрейм (или вызов функции в определенной среде) в стеке, затем удалять его и переходить к следующему.

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

  • Добавить first() в стек, выполнить first(), который записывает 1 в консоль, удалить first() из стека.
  • Добавить second() в стек, выполнить second(), который записывает 2 в консоль, удалить second() из стека.
  • Добавить third() в стек, выполнить third(), который записывает 3 в консоль, удалить third() из стека.

Второй пример с setTimout выглядит так:

  • Добавить first() в стек, выполнить first(), который записывает 1 в консоль, удалить first() из стека.
  • Добавить second() в стек, выполнить second().
    • Добавить setTimeout() в стек, выполнить setTimeout() Web API, который запускает таймер и добавляет анонимную функцию в очередь, удалить setTimeout() из стека.
  • Удалите second() из стека.
  • Добавьте third() в стек, запустите third(), который регистрирует 3 в консоль, удалите third() из стека.
  • Цикл событий проверяет очередь на наличие ожидающих сообщений и находит анонимную функцию от setTimeout(), добавляет функцию в стек, который регистрирует 2 в консоль, а затем удаляет его из стека.

Использование setTimeout, асинхронного веб-API, вводит понятие очереди, о котором будет рассказано в следующем уроке.

Очередь

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

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

Примечание: Существует также другая очередь, называемая очередью задач или очередью микрозадач, которая обрабатывает обещания. Микрозадачи, такие как обещания, обрабатываются с более высоким приоритетом, чем макрозадачи, такие как setTimeout.

Теперь вы знаете, как цикл событий использует стек и очередь для управления порядком выполнения кода. Следующая задача – выяснить, как контролировать порядок выполнения в вашем коде. Для этого сначала вы узнаете о первоначальном способе гарантировать правильную обработку асинхронного кода циклом событий: обратные вызовы.

Обратные вызовы

В примере с setTimeout функция с таймаутом запускается после выполнения всего в основном контексте выполнения верхнего уровня. Но если вы хотите, чтобы одна из функций, например, функция third, запускалась после таймаута, то вам придется использовать асинхронные методы кодирования. В данном случае таймаут может представлять собой асинхронный вызов API, содержащий данные. Вы хотите работать с данными из вызова API, но вы должны убедиться, что данные вернутся сначала.

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

Вот синтаксический пример кода функции высшего порядка и обратного вызова:

// Функция
function fn() {
  console.log('Just a function')
}

// Функция, которая принимает другую функцию в качестве аргумента
function higherOrderFunction(callback) {
  // Когда вы вызываете функцию, переданную как аргумент, это называется обратным вызовом
  callback()
}

// Передача функции
higherOrderFunction(fn)

В этом коде вы определяете функцию fn, определяете функцию higherOrderFunction, которая принимает функцию callback в качестве аргумента, и передаете fn как обратный вызов для higherOrderFunction.

Запуск этого кода приведет к следующему:

Output
Just a function

Вернемся к функциям first, second и third с использованием setTimeout. Вот что у вас есть на данный момент:

function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

Задача состоит в том, чтобы функция third всегда откладывала выполнение до завершения асинхронного действия в функции second. Здесь на помощь приходят обратные вызовы. Вместо выполнения first, second и third на верхнем уровне выполнения вы передадите функцию third в качестве аргумента в second. Функция second выполнит обратный вызов после завершения асинхронного действия.

Вот три функции с применением обратного вызова:

// Определение трех функций
function first() {
  console.log(1)
}

function second(callback) {
  setTimeout(() => {
    console.log(2)

    // Выполнение функции обратного вызова
    callback()
  }, 0)
}

function third() {
  console.log(3)
}

Теперь выполните first и second, затем передайте third в качестве аргумента в second:

first()
second(third)

После выполнения этого блока кода вы получите следующий вывод:

Output
1 2 3

Сначала будет выведено 1, а после завершения таймера (в данном случае нулевое количество секунд, но вы можете изменить его на любое другое) будут выведены 2 и затем 3. Передавая функцию в качестве обратного вызова, вы успешно задерживаете выполнение функции до завершения асинхронного веб-API (setTimeout).

Главное здесь заключается в том, что обратные вызовы не являются асинхронными — setTimeout является асинхронным веб-API, ответственным за обработку асинхронных задач. Обратный вызов просто позволяет вам быть проинформированным о том, когда асинхронная задача завершится, и обрабатывает успех или неудачу задачи.

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

Вложенные обратные вызовы и пирамида неудачи

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

Вот демонстрация вложенных обратных вызовов:

function pyramidOfDoom() {
  setTimeout(() => {
    console.log(1)
    setTimeout(() => {
      console.log(2)
      setTimeout(() => {
        console.log(3)
      }, 500)
    }, 2000)
  }, 1000)
}

В этом коде каждый новый setTimeout вложен в функцию более высокого порядка, создавая пирамидальную структуру из всё более глубоких обратных вызовов. Запуск этого кода даст следующий результат:

Output
1 2 3

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

Вот пример более реалистичной “пирамиды ада”, с которой вы можете поиграть:

// Пример асинхронной функции
function asynchronousRequest(args, callback) {
  // Выбрасывать ошибку, если не переданы аргументы
  if (!args) {
    return callback(new Error('Whoa! Something went wrong.'))
  } else {
    return setTimeout(
      // Просто добавляем случайное число, чтобы казалось, что утрированная асинхронная функция
      // возвращает разные данные
      () => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
      500,
    )
  }
}

// Вложенные асинхронные запросы
function callbackHell() {
  asynchronousRequest('First', function first(error, response) {
    if (error) {
      console.log(error)
      return
    }
    console.log(response.body)
    asynchronousRequest('Second', function second(error, response) {
      if (error) {
        console.log(error)
        return
      }
      console.log(response.body)
      asynchronousRequest(null, function third(error, response) {
        if (error) {
          console.log(error)
          return
        }
        console.log(response.body)
      })
    })
  })
}

// Выполнить
callbackHell()

В этом коде вы должны учитывать возможные response и error в каждой функции, делая функцию callbackHell визуально запутанной.

Запустив этот код, вы получите следующее:

Output
First 9 Second 3 Error: Whoa! Something went wrong. at asynchronousRequest (<anonymous>:4:21) at second (<anonymous>:29:7) at <anonymous>:9:13

Этот способ обработки асинхронного кода сложно следить. В результате было введено понятие промисов в ES6. Об этом пойдет речь в следующем разделе.

Промисы

A promise represents the completion of an asynchronous function. It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume. This tutorial will show you how to do both.

Создание обещания

Вы можете инициализировать обещание с помощью синтаксиса new Promise, и вы должны инициализировать его с помощью функции. Функция, которая передается обещанию, имеет параметры resolve и reject. Функции resolve и reject обрабатывают успешное и неудачное завершение операции, соответственно.

Напишите следующую строку для объявления обещания:

// Инициализация обещания
const promise = new Promise((resolve, reject) => {})

Если вы проверите инициализированное обещание в этом состоянии с помощью консоли вашего веб-браузера, вы обнаружите, что оно имеет статус pending и значение undefined:

Output
__proto__: Promise [[PromiseStatus]]: "pending" [[PromiseValue]]: undefined

Пока что ничего не настроено для обещания, поэтому оно будет висеть в состоянии pending вечно. Первое, что вы можете сделать для тестирования обещания, это выполнить обещание, разрешив его значением:

const promise = new Promise((resolve, reject) => {
  resolve('We did it!')
})

Теперь, проверив обещание, вы обнаружите, что у него статус fulfilled, и value установлено на значение, которое вы передали в resolve:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: "We did it!"

Как утверждалось в начале этого раздела, обещание – это объект, который может возвращать значение. После успешного выполнения value изменяется с undefined на данные.

A promise can have three possible states: pending, fulfilled, and rejected.

  • Ожидание – Исходное состояние перед разрешением или отклонением
  • Выполнено – Успешная операция, обещание выполнено
  • Отклонено – Операция не удалась, обещание отклонено

После того как обещание выполнено или отклонено, оно завершено.

Теперь, когда у вас есть представление о том, как создаются обещания, давайте посмотрим, как разработчик может использовать эти обещания.

Потребление обещания

Обещание в последнем разделе было выполнено с значением, но вы также хотите иметь возможность получить доступ к этому значению. У обещаний есть метод под названием then, который будет запускаться после того, как обещание достигнет состояния resolve в коде. then вернет значение обещания в качестве параметра.

Вот как вы бы вернули и записали value образца обещания:

promise.then((response) => {
  console.log(response)
})

У обещания, которое вы создали, было [[PromiseValue]] равное We did it!. Это значение будет передано в анонимную функцию как response:

Output
We did it!

До сих пор созданный вами пример не включал асинхронного веб-API — он только объяснил, как создавать, разрешать и потреблять обычное обещание JavaScript. Используя setTimeout, вы можете протестировать асинхронный запрос.

Следующий код моделирует данные, возвращаемые из асинхронного запроса в виде обещания:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})

// Записать результат
promise.then((response) => {
  console.log(response)
})

Использование синтаксиса then гарантирует, что response будет зарегистрирован только после завершения операции setTimeout через 2000 миллисекунд. Все это происходит без вложенных обратных вызовов.

Теперь через две секунды обещание будет разрешено, и его значение будет зарегистрировано в then:

Output
Resolving an asynchronous request!

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

// Связать обещание
promise
  .then((firstResponse) => {
    // Вернуть новое значение для следующего then
    return firstResponse + ' And chaining!'
  })
  .then((secondResponse) => {
    console.log(secondResponse)
  })

Выполненный ответ во втором then зарегистрирует возвращаемое значение:

Output
Resolving an asynchronous request! And chaining!

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

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

До сих пор вы обрабатывали только обещание с успешным resolve, которое помещает обещание в состояние fulfilled. Но часто с асинхронным запросом вам также нужно обрабатывать ошибку – если API недоступен, или отправлен неправильный или неавторизованный запрос. Обещание должно быть способно обрабатывать оба случая. В этом разделе вы создадите функцию для тестирования как успешного, так и ошибочного случаев создания и использования обещания.

Эта функция getUsers будет передавать флаг обещанию и возвращать обещание:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Обработка resolve и reject в асинхронном API
    }, 1000)
  })
}

Настройте код так, чтобы если onSuccess равно true, тайм-аут будет выполнен с какими-то данными. Если false, функция будет отклонена с ошибкой:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Обработка resolve и reject в асинхронном API
      if (onSuccess) {
        resolve([
          {id: 1, name: 'Jerry'},
          {id: 2, name: 'Elaine'},
          {id: 3, name: 'George'},
        ])
      } else {
        reject('Failed to fetch data!')
      }
    }, 1000)
  })
}

Для успешного результата возвращайте JavaScript объекты, представляющие примерные данные пользователя.

Для обработки ошибки вы будете использовать метод catch экземпляра. Это даст вам обратный вызов с ошибкой в качестве параметра error.

Запустите команду getUser с установленным onSuccess в false, используя метод then для успешного случая и метод catch для ошибки:

// Запустить функцию getUsers с флагом false для вызова ошибки
getUsers(false)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

Поскольку произошла ошибка, блок then будет пропущен, и ошибку обработает блок catch:

Output
Failed to fetch data!

Если вы переключите флаг и вместо этого используете resolve, блок catch будет проигнорирован, и вместо этого будет возвращены данные:

// Запуск функции getUsers с флагом true для успешного разрешения
getUsers(true)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

Это приведет к получению данных о пользователе:

Output
(3) [{…}, {…}, {…}] 0: {id: 1, name: "Jerry"} 1: {id: 2, name: "Elaine"} 3: {id: 3, name: "George"}

Для справки вот таблица с методами обработчиков на объектах Promise:

Method Description
then() Handles a resolve. Returns a promise, and calls onFulfilled function asynchronously
catch() Handles a reject. Returns a promise, and calls onRejected function asynchronously
finally() Called when a promise is settled. Returns a promise, and calls onFinally function asynchronously

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

В последнем разделе о промисах этот учебник приведет типичный пример использования API веб-запросов, который возвращает обещания: API Fetch.

Использование API Fetch с обещаниями

Один из самых полезных и часто используемых веб-API, возвращающих обещание, – это Fetch API, который позволяет делать асинхронный запрос к ресурсу через сеть. fetch состоит из двух частей и, следовательно, требует цепочки then. В этом примере демонстрируется обращение к API GitHub для получения данных пользователя, а также обработка возможных ошибок:

// Получение пользователя из API GitHub
fetch('https://api.github.com/users/octocat')
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.error(error)
  })

Запрос fetch отправляется на URL https://api.github.com/users/octocat, который асинхронно ожидает ответа. Первый then передает ответ анонимной функции, которая форматирует ответ как JSON данные, затем передает JSON во второй then, который записывает данные в консоль. Оператор catch записывает любую ошибку в консоль.

Запуск этого кода приведет к следующему:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

Это запрошенные данные с https://api.github.com/users/octocat, отображенные в формате JSON.

Этот раздел учебника показал, что промисы включают в себя множество улучшений для работы с асинхронным кодом. Однако, хотя использование then для обработки асинхронных действий легче следить, чем пирамида обратных вызовов, некоторые разработчики по-прежнему предпочитают синхронный формат написания асинхронного кода. Чтобы удовлетворить эту потребность, ECMAScript 2016 (ES7) ввела функции async и ключевое слово await, чтобы упростить работу с промисами.

Асинхронные функции с async/await

Функция async позволяет обрабатывать асинхронный код таким образом, что он выглядит синхронным. Функции async все еще используют промисы внутри, но имеют более традиционный синтаксис JavaScript. В этом разделе вы попробуете примеры этого синтаксиса.

Вы можете создать функцию async, добавив ключевое слово async перед функцией:

// Создание асинхронной функции
async function getUser() {
  return {}
}

Хотя эта функция пока что не обрабатывает ничего асинхронного, она ведет себя по-другому, чем традиционная функция. Если вы выполните эту функцию, вы увидите, что она возвращает промис с [[PromiseStatus]] и [[PromiseValue]], а не возвращаемое значение.

Попробуйте это, залогировав вызов функции getUser:

console.log(getUser())

Это даст следующий результат:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: Object

Это означает, что вы можете обрабатывать функцию async с помощью then так же, как вы могли бы обрабатывать промис. Попробуйте это с помощью следующего кода:

getUser().then((response) => console.log(response))

Этот вызов getUser передает возвращаемое значение анонимной функции, которая логирует значение в консоль.

Вы получите следующий результат при запуске этой программы:

Output
{}

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

С этим знанием вы можете переписать запрос Fetch из последнего раздела, используя async/await следующим образом:

// Обработка fetch с использованием async/await
async function getUser() {
  const response = await fetch('https://api.github.com/users/octocat')
  const data = await response.json()

  console.log(data)
}

// Выполнение асинхронной функции
getUser()

Операторы await здесь обеспечивают, что data не будет зарегистрирована до того, как запрос заполнит её данными.

Теперь окончательные data можно обрабатывать внутри функции getUser, без необходимости использования then. Вот вывод регистрации data:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

Примечание: Во многих средах async необходим для использования await—однако некоторые новые версии браузеров и Node позволяют использовать верхнеуровневый await, что позволяет обойти создание асинхронной функции для обёртки await.

Наконец, поскольку вы обрабатываете разрешённое обещание внутри асинхронной функции, вы также можете обработать ошибку внутри функции. Вместо использования метода catch с then, вы будете использовать шаблон try/catch, чтобы обработать исключение.

Добавьте следующий выделенный код:

// Обработка успеха и ошибок с использованием async/await
async function getUser() {
  try {
    // Обработка успеха в блоке try
    const response = await fetch('https://api.github.com/users/octocat')
    const data = await response.json()

    console.log(data)
  } catch (error) {
    // Обработка ошибки в блоке catch
    console.error(error)
  }
}

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

Современный асинхронный код на JavaScript обычно обрабатывается с использованием синтаксиса async/await, но важно иметь рабочее понимание того, как работают промисы, особенно поскольку промисы способны к дополнительным функциям, которые нельзя обрабатывать с помощью async/await, например, объединение промисов с помощью Promise.all().

Примечание: async/await можно воспроизвести с использованием генераторов в сочетании с промисами, чтобы добавить больше гибкости в ваш код. Чтобы узнать больше, ознакомьтесь с нашим учебным пособием Понимание генераторов в JavaScript.

Вывод

Поскольку веб-API часто предоставляют данные асинхронно, изучение того, как обрабатывать результат асинхронных действий, является важной частью работы разработчика JavaScript. В этой статье вы узнали, как хост-среда использует цикл событий для управления порядком выполнения кода с помощью стека и очереди. Вы также попробовали примеры трех способов обработки успеха или неудачи асинхронного события с помощью обратных вызовов, промисов и синтаксиса async/await. Наконец, вы использовали веб-API Fetch для обработки асинхронных действий.

Для получения дополнительной информации о том, как браузер обрабатывает параллельные события, прочтите Модель параллелизма и цикл событий на сайте Mozilla Developer Network. Если вы хотите узнать больше о JavaScript, вернитесь к нашей серии Как писать на JavaScript.

Source:
https://www.digitalocean.com/community/tutorials/understanding-the-event-loop-callbacks-promises-and-async-await-in-javascript