7 причин почему JavaScript Async/Await вытесняет прочь промисы (Руководство)

Anna Melnyk
Nov 3 · 6 min read

Перевод «7 Reasons Why JavaScript Async/Await Is Better Than Plain Promises (Tutorial)» Mostafa Gaafar


Async/await был представлен в NodeJS 7.6 и в настоящее время поддерживается всеми современными браузерами. Я верю, что это единственное грандиозное нововведение в JS с 2017 года. Если вы в замешательстве, вот несколько причин с примерами, почему вы должны принять его немедленно и никогда не оглядываться назад.

Async/Await 101

Для тех, кто еще не слышал об Async/Await ранее, приведено быстрое введение:

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

Синтаксис

Предположим есть функция getJSON, которая возвращает промис, и этот промис выполнится с некоторым JSON объектом. Мы хотим вызвать функцию и вывести этот JSON, а затем вернуть “done”.

Вот так бы мы реализовали ее, используя промисы

const makeRequest = () =>
getJSON()
.then(data => {
console.log(data)
return "done"
})

makeRequest()

И вот как будет выглядить реализация с использованием async/await

const makeRequest = async () => {
console.log(await getJSON())
return "done"
}

makeRequest()

Здесь имеется несколько отличий:

1. Перед функцией стоит ключевое слово async. Await может быть использован только внутри функции, определенной с использованием async. Любая async/await функция неявно возвращает промис, и результатом будет значение выполненного промиса, что бы мы не возвращали из функции (например, строку “done” в нашем случае).

2. Пункт выше подразумевает, что мы не можем использовать await на верхнем уровне нашего кода, поскольку он не находится внутри async функции.

// this will not work in top level
// await makeRequest()

// this will work
makeRequest().then((result) => {
// do something
})

3. await getJSON() означает, что вызов console.log будет ждать пока getJSON() промис выполнится и выведет значение.

Почему это лучше?

1. Коротко и чисто

Взгляните, сколько коды мы не написали! Даже в замудренном примере выше видно, какое мы сократили количество кода. Нам не нужно писать .then, создавать анонимную функцию, чтобы обработать ответ, или давать название переменной, которую нам не нужно использовать. Мы также избежали вложенности кода. Эти небольшие преимущества станут более очевидным в следующих примерах кода.

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

Async/await наконец-то делает возможным обрабатывать синхронные и асинхронные ошибки с той же конструкцией, старым добрым try/catch. В примере c промисами ниже, the try/catch не обработает, если JSON.parse не выполнится, так как это происходит внутри промиса. Нам нужно вызвать .catch промиса и продублировать код по обработке ошибки, который (надеюсь) будет более усложненный, чем console.log в вашем готовом коде.

const makeRequest = () => {
try {
getJSON()
.then(result => {
// this parse may fail
const data = JSON.parse(result)
console.log(data)
})
// uncomment this block to handle asynchronous errors
// .catch((err) => {
// console.log(err)
// })
} catch (err) {
console.log(err)
}
}

Тепереь рассмотрим такой же код, написанный с помощью async/await. Блок catch теперь будет обрабатывать ошибки парсинга.

const makeRequest = async () => {
try {
// this parse may fail
const data = JSON.parse(await getJSON())
console.log(data)
} catch (err) {
console.log(err)
}
}

3. Условия

Нижк представлен код, в котором получены какие-то данные, и решается — стоит вернуть их, или получить детали, из значений полученных данных.

const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
console.log(moreData)
return moreData
})
} else {
console.log(data)
return data
}
})
}

Рассматривая этот код, можно обзавестись головной болью. Легко запутаться в приведенных вложенностях (6 уровней), скобках и возвращаемых значениях, которые нужны только для поднятия окончательного результата до главного промиса.

Пример ниже становится более наглядным, после переписывания на async/await.

const makeRequest = async () => {
const data = await getJSON()
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
console.log(moreData)
return moreData
} else {
console.log(data)
return data
}
}

4. Промежуточные значения

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

const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return promise2(value1)
.then(value2 => {
// do something
return promise3(value1, value2)
})
})
}

Если promise3 не требует value1, будет немного проще упростить вложенность. Если вы из тех людей,которых не устроит подобный код, вы можете обернуть value1 и value2 в Promise.all, и таким образом, получить вложенность меньше, вот так

const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return Promise.all([value1, promise2(value1)])
})
.then(([value1, value2]) => {
// do something
return promise3(value1, value2)
})
}

Такой подход жертвует семантикой ради читабельности кода. Нет другой причины помещать value1 & value2 в массив вместе, кроме избежания вложенности промисов.

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

const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}

5. Ошибки

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

const makeRequest = () => {
return callAPromise()
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => {
throw new Error("oops");
})
}

makeRequest()
.catch(err => {
console.log(err);
// output
// Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
})

Стек ошибок, возвращаемый из цепочки промисов, не дает представления о том, в каком месте произошла ошибка. Хуже того, это вводит в заблуждение; единственное имя функции, которое она содержит, — это callAPromise, который абсолютно не причем к этой ошибке (хотя файл и номер строки по-прежнему полезны).

Однако стек ошибок из async/await указывает на функцию, которая содержит ошибку

const makeRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
throw new Error("oops");
}

makeRequest()
.catch(err => {
console.log(err);
// output
// Error: oops at makeRequest (index.js:7:9)
})

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

6. Отладка

Убийственным преимуществом использования async/await является простая отладка. Отлаживание промисов всегда было болезненным процессом по 2 причинам

  1. Вы можете установить брейкпоинт в стрелочной функции, который вернет выражения (без тела).
Попробуйте установить брейкпоинт в любом месте

2. Если вы установите брейкпоинт внутри блока .then, отладчик не будет переходить к следующему .then, так как он “шагает” только по синхронному коду.

С async/await вам больше не нужны стрелочные функции, и вы можете выполнить шаг через await так, как если бы это были обычные синхронные вызовы.

7. Вы можете подождать, что угодно

И, наконец, await может применяться как для синхронных, так и асинхронных выражений. Например, вы можете написать await 5, что будет эквивалентно Promise.resolve(5). На первый взгляд, такая запись не выглядит весьма полезной, но станет большим преимуществом, когда при написании библиотеки или вспомогательной функции, вы не знаете будет ли она использоваться как синхронная или асинхронная.

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

const recordTime = (makeRequest) => {
const timeStart = Date.now();
makeRequest().then(() => { // throws error for sync functions (.then is not a function)
const timeEnd = Date.now();
console.log('time take:', timeEnd - timeStart);
})
}

Известно, что все API вызовы возвращают промис, но что произойдет, если вы используете ту же функцию для записи времени, затраченного на синхронную функцию? Будет выброшена ошибка, потому что синхронная функция не возвращает промис. Обычным способом избежать такого поведения является обертывание makeRequest () в Promise.resolve ().

Если вы используете async/await, не стоит волноваться о таких случаях, так как await безопасно отработает с любым значением

const recordTime = async (makeRequest) => {
const timeStart = Date.now();
await makeRequest(); // works for any sync or async function
const timeEnd = Date.now();
console.log('time take:', timeEnd - timeStart);
}

В заключение

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

Выводы

Некоторый скептицизм, который может присутствовать в использовании async/await, заключается в том, что он делает асинхронный код менее очевидным: мы научились распознавать асинхронный код всякий раз, когда мы видим функцию обратного вызова (callback) или .then. Теперь нам потребуется время, чтобы приспособиться к новым признакам асинхронного кода, но в C# такая функция существует в течение многих лет, и люди, которые знакомые с данным языком программирования, знают, что это небольшое, временное неудобство.

Anna Melnyk

Written by

Подрастающий фронтендер. Изучаю Js, React, перевожу статьи…

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade