Async await catch error

I like the flatness of the new Async/Await feature available in Typescript, etc. However, I'm not sure I like the fact that I have to declare the variable I'm awaiting on the outside of a try...catch

Cleaner code

using async/await with Promise catch handler.

From what I see, this has been a long-standing problem that has bugged (both meanings) many programmers and their code. The Promise .catch is really no different from try/catch.

Working harmoniously with await/async, ES6 Promise’s catch handler provides a proper solution and make code cleaner:

const createUser = await this.User
    .create(userInfo)
    .catch(error => console.error(error))

console.log(createdUser)
// business
// logic
// goes
// here

Note that while this answers the question, it gobbles up the error. The intention must be for the execution to continue and not throw. In this case, it’s usually always better to be explicit and return false from catch and check for user:

    .catch(error => { 
        console.error(error); 
        return false 
    })

if (!createdUser) // stop operation

In this case, it is better to throw because (1) this operation (creating a user) is not expected to failed, and (2) you are likely not able to continue:

const createUser = await this.User
    .create(userInfo)
    .catch(error => {
        // do what you need with the error
        console.error(error)

        // maybe send to Datadog or Sentry

        // don't gobble up the error
        throw error
    })

console.log(createdUser)
// business
// logic
// goes
// here

Learning catch doesn’t seem like worth it?

The cleanliness benefits may not be apparent above, but it adds up in real-world complex async operations.

As an illustration, besides creating user (this.User.create), we can push notification (this.pushNotification) and send email (this.sendEmail).

this.User.create

this.User.create = async(userInfo) => {

    // collect some fb data and do some background check in parallel
    const facebookDetails = await retrieveFacebookAsync(userInfo.email)
        .catch(error => {
            // we can do some special error handling

            // and throw back the error
         })
    const backgroundCheck = await backgroundCheckAsync(userInfo.passportID)

    if (backgroundCheck.pass !== true) throw Error('Background check failed')

    // now we can insert everything
    const createdUser = await Database.insert({ ...userInfo, ...facebookDetails })

    return createdUser
}

this.pushNotifcation and this.sendEmail

this.pushNotification = async(userInfo) => {
    const pushed = await PushNotificationProvider.send(userInfo)
    return pushed
})

this.sendEmail = async(userInfo) => {
    const sent = await mail({ to: userInfo.email, message: 'Welcome' })
    return sent
})

Compose the operations:

const createdUser = await this.User
    .create(userInfo)
    .catch(error => {
        // handle error
    })

// business logic here

return await Promise.all([
    this.pushNotification(userInfo),
    this.sendEmail(userInfo)
]).catch(error => {
    // handle errors caused
    // by pushNotification or sendEmail
})

No try/catch. And it’s clear what errors you are handling.

Время прочтения
9 мин

Просмотры 52K

Автор статьи разбирает на примерах Async/Await в JavaScript. В целом, Async/Await — удобный способ написания асинхронного кода. До появления этой возможности подобный код писали с использованием коллбэков и промисов. Автор оригинальной статьи раскрывает преимущества Async/Await, разбирая различные примеры.

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

Skillbox рекомендует: Образовательный онлайн-курс «Java-разработчик».

Callback

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

Вот пример асинхронного чтения файла на Node.js:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

Проблемы возникают в тот момент, когда требуется выполнить сразу несколько асинхронных операций. Давайте представим себе вот такой сценарий: выполняется запрос в БД пользователя Arfat, нужно считать его поле profile_img_url и загрузить картинку с сервера someserver.com.
После загрузки конвертируем изображение в иной формат, например из PNG в JPEG. Если конвертация прошла успешно, на почту пользователя отправляется письмо. Далее информация о событии заносится в файл transformations.log с указанием даты.

Стоит обратить внимание на наложенность обратных вызовов и большое количество }) в финальной части кода. Это называется Callback Hell или Pyramid of Doom.

Недостатки такого способа очевидны:

  • Этот код сложно читать.
  • В нем также сложно обрабатывать ошибки, что зачастую приводит к ухудшению качества кода.

Для того чтобы решить эту проблему, в JavaScript были добавлены промисы. Они позволяют заменить глубокую вложенность коллбэков словом .then.

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

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

Вот задача, которая покажет значение последнего пункта.

Предположим, что есть цикл for, выводящий последовательность чисел от 0 до 10 со случайным интервалом (0–n секунд). Используя промисы, нужно изменить этот цикл таким образом, чтобы числа выводились в последовательности от 0 до 10. Так, если вывод нуля занимает 6 секунд, а единицы — 2 секунды, сначала должен быть выведен ноль, а потом уже начнется отсчет вывода единицы.

И конечно, для решения этой задачи мы не используем Async/Await либо .sort. Пример решения — в конце.

Async-функции

Добавление async-функций в ES2017 (ES8) упростило задачу работы с промисами. Отмечу, что async-функции работают «поверх» промисов. Эти функции не представляют собой качественно другие концепции. Async-функции задумывались как альтернатива коду, который использует промисы.

Async/Await дает возможность организовать работу с асинхронным кодом в синхронном стиле.

Таким образом, знание промисов облегчает понимание принципов Async/Await.

Синтаксис

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

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

Async вставляется в самом начале объявления функции, а в случае использования стрелочной функции — между знаком «=» и скобками.

Эти функции можно поместить в объект в качестве методов либо же использовать в объявлении класса.

// As an object's method
 
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}
 
// In a class
 
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

NB! Стоит помнить, что конструкторы класса и геттеры/сеттеры не могут быть асинхронными.

Семантика и правила выполнения

Async-функции, в принципе, похожи на стандартные JS-функции, но есть и исключения.

Так, async-функции всегда возвращают промисы:

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

В частности, fn возвращает строку hello. Ну а поскольку это асинхронная функция, то значение строки обертывается в промис при помощи конструктора.

Вот альтернативная конструкция без Async:

function fn() {
  return Promise.resolve('hello');
}
 
fn().then(console.log);
// hello

В этом случае возвращение промиса производится «вручную». Асинхронная функция всегда обертывается в новый промис.

В том случае, если возвращаемое значение — примитив, async-функция выполняет возврат значения, обертывая его в промис. В том случае, если возвращаемое значение и есть объект промиса, его решение возвращается в новом промисе.

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

Но что произойдет в том случае, если внутри асинхронной функции окажется ошибка?

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

Если она не будет обработана, foo() вернет промис с реджектом. В этой ситуации вместо Promise.resolve вернется Promise.reject, содержащий ошибку.

Async-функции на выходе всегда дают промис, вне зависимости от того, что возвращается.

Асинхронные функции приостанавливаются при каждом await .

Await влияет на выражения. Так, если выражение является промисом, async-функция приостанавливается до момента выполнения промиса. В том случае, если выражение не является промисом, оно конвертируется в промис через Promise.resolve и потом завершается.

// utility function to cause delay
// and get random value
 
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};
 
async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
 
  return a + b * c;
}
 
// Execute fn
fn().then(console.log);

А вот описание того, как работает fn-функция.

  • После ее вызова первая строка конвертируется из const a = await 9; в const a = await Promise.resolve(9);.
  • После использования Await выполнение функции приостанавливается, пока а не получает свое значение (в текущей ситуации это 9).
  • delayAndGetRandom(1000) приостанавливает выполнение fn-функции, пока не завершится сама (после 1 секунды). Это фактически является остановкой fn-функции на 1 секунду.
  • delayAndGetRandom(1000) через resolve возвращает случайное значение, которое затем присваивается переменной b.
  • Ну а случай с переменной с аналогичен случаю с переменной а. После этого все останавливается на секунду, но теперь delayAndGetRandom(1000) ничего не возвращает, поскольку этого не требуется.
  • В итоге значения считаются по формуле a + b * c. Результат же обертывается в промис при помощи Promise.resolve и возвращается функцией.

Эти паузы могут напоминать генераторы в ES6, но этому есть свои причины.

Решаем задачу

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

В функции finishMyTask используется Await для ожидания результатов таких операций, как queryDatabase, sendEmail, logTaskInFile и других. Если же сравнивать это решение с тем, где использовались промисы, станет очевидным сходство. Тем не менее версия с Async/Await довольно сильно упрощает все синтаксические сложности. В этом случае нет большого количества коллбэков и цепочек вроде .then/.catch.

Вот решение с выводом чисел, здесь есть два варианта.

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
 
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});
 
// Implementation Two (Using Recursion)
 
const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {
 
    if (i === 10) {
      return undefined;
    }
 
    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

А вот решение с использованием async-функций.

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

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

Необработанные ошибки обертываются в rejected промис. Тем не менее в async-функциях можно использовать конструкцию try/catch для того, чтобы выполнить синхронную обработку ошибок.

async function canRejectOrReturn() {
  // wait one second
  await new Promise(res => setTimeout(res, 1000));
 
// Reject with ~50% probability
  if (Math.random() > 0.5) {
    throw new Error('Sorry, number too big.')
  }
 
return 'perfect number';
}

canRejectOrReturn() — это асинхронная функция, которая либо удачно выполняется (“perfect number”), либо неудачно завершается с ошибкой (“Sorry, number too big”).

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Поскольку в примере выше ожидается выполнение canRejectOrReturn, то собственное неудачное завершение повлечет за собой исполнение блока catch. В результате функция foo завершится либо с undefined (когда в блоке try ничего не возвращается), либо с error caught. В итоге у этой функции не будет неудачного завершения, поскольку try/catch займется обработкой самой функции foo.

Вот еще пример:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Стоит уделить внимание тому, что в примере из foo возвращается canRejectOrReturn. Foo в этом случае завершается либо perfect number, либо возвращается ошибка Error (“Sorry, number too big”). Блок catch никогда не будет исполняться.

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

try {
    const promise = canRejectOrReturn();
    return promise;
}

А вот что будет, если использовать вместе await и return:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

В коде выше foo удачно завершится как с perfect number, так и с error caught. Здесь отказов не будет. Но foo завершится с canRejectOrReturn, а не с undefined. Давайте убедимся в этом, убрав строку return await canRejectOrReturn():

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

Распространенные ошибки и подводные камни

В некоторых случаях использование Async/Await может приводить к ошибкам.

Забытый await

Такое случается достаточно часто — перед промисом забывается ключевое слово await:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

В коде, как видно, нет ни await, ни return. Поэтому foo всегда завершается с undefined без задержки в 1 секунду. Но промис будет выполняться. Если же он выдает ошибку или реджект, то в этом случае будет вызываться UnhandledPromiseRejectionWarning.

Async-функции в обратных вызовах

Async-функции довольно часто используются в .map или .filter в качестве коллбэков. В качестве примера можно привести функцию fetchPublicReposCount(username), которая возвращает количество открытых на GitHub репозиториев. Допустим, есть три пользователя, чьи показатели нам нужны. Вот код для этой задачи:

const url = 'https://api.github.com/users';
 
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

Нам нужны аккаунты ArfatSalman, octocat, norvig. В этом случае выполняем:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
 
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Стоит обратить внимание на Await в обратном вызове .map. Здесь counts — массив промисов, ну а .map — анонимный обратный вызов для каждого указанного пользователя.

Чрезмерно последовательное использование await

В качестве примера возьмем такой код:

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}

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

Если, например, на обработку одного пользователя уходит около 300 мс, то для всех пользователей это уже секунда, затрачиваемое время линейно зависит от числа пользователей. Но раз получение количества репо не зависит друг от друга, процессы можно распараллелить. Для этого нужна работа с .map и Promise.all:

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.all на входе получает массив промисов с возвращением промиса. Последний после завершения всех промисов в массиве или при первом реджекте завершается. Может случиться так, что все они не запустятся одновременно, — для того чтобы обеспечить одновременный запуск, можно использовать p-map.

Заключение

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

Skillbox рекомендует:

  • Практический курс «Мобильный разработчик PRO».
  • Прикладной онлайн-курс «Аналитик данных на Python».
  • Двухлетний практический курс «Я — Веб-разработчик PRO».
  • Назад
  • Обзор: Asynchronous
  • Далее (en-US)

В ECMAScript версии 2017 появились async functions и ключевое слово await (ECMAScript Next support in Mozilla). По существу, такие функции есть синтаксический сахар над Promises и Generator functions (ts39). С их помощью легче писать/читать асинхронный код, ведь они позволяют использовать привычный синхронный стиль написания. В этой статье мы на базовом уровне разберёмся в их устройстве.

Примечания: Чтобы лучше понять материал, желательно перед чтением ознакомиться с основами JavaScript, асинхронными операциями вообще и объектами Promises.
Цель материала: Научить писать современный асинхронный код с использованием Promises и async functions.

Основы async/await

Ключевое слово async

Ключевое слово async позволяет сделать из обычной функции (function declaration или function expression) асинхронную функцию (async function). Такая функция делает две вещи:
— Оборачивает возвращаемое значение в Promise
— Позволяет использовать ключевое слово await (см. дальше)

Попробуйте выполнить в консоли браузера следующий код:

function hello() { return "Hello" };
hello();

Функция возвращает «Hello» — ничего необычного, верно ?

Но что если мы сделаем её асинхронной ? Проверим:

async function hello() { return "Hello" };
hello();

Как было сказано ранее, вызов асинхронной функции возвращает объект Promise.

Вот пример с async function expression:

let hello = async function() { return "Hello" };
hello();

Также можно использовать стрелочные функции:

let hello = async () => { return "Hello" };

Все они в общем случае делают одно и то же.

Чтобы получить значение, которое возвращает Promise, мы как обычно можем использовать метод .then():

hello().then((value) => console.log(value))

или ещё короче

hello().then(console.log)

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

Ключевое слово await

Асинхронные функции становятся по настоящему мощными, когда вы используете ключевое слово await — по факту, await работает только в асинхронных функциях. Мы можем использовать await перед promise-based функцией, чтобы остановить поток выполнения и дождаться результата её выполнения (результат Promise). В то же время, остальной код нашего приложения не блокируется и продолжает работать.

Вы можете использовать await перед любой функцией, что возвращает Promise, включая Browser API функции.

Небольшой пример:

async function hello() {
  return greeting = await Promise.resolve("Hello");
};

hello().then(alert);

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

Переписываем Promises с использованием async/await

Давайте посмотрим на пример из предыдущей статьи:

fetch('coffee.jpg')
.then(response => {
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    return response.blob();
  }
})
.then(myBlob => {
  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
})
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

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

async function myFetch() {
  let response = await fetch('coffee.jpg');

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    let myBlob = await response.blob();

    let objectURL = URL.createObjectURL(myBlob);
    let image = document.createElement('img');
    image.src = objectURL;
    document.body.appendChild(image);
  }
}

myFetch()
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

Согласитесь, что код стал короче и понятнее — больше никаких блоков .then() по всему скрипту!

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

async function myFetch() {
  let response = await fetch('coffee.jpg');
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    return await response.blob();
  }
}

myFetch().then((blob) => {
  let objectURL = URL.createObjectURL(blob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}).catch(e => console.log(e));

Можете попрактиковаться самостоятельно, или запустить наш live example (а также source code).

Минуточку, а как это все работает ?

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

Внутри myFetch() находится код, который слегка напоминает версию на Promise, но есть важные отличия. Вместо того, чтобы писать цепочку блоков .then() мы просто использует ключевое слово await перед вызовом promise-based функции и присваиваем результат в переменную. Ключевое слово await говорит JavaScript runtime приостановить код в этой строке, не блокируя остальной код скрипта за пределами асинхронной функции. Когда вызов promise-based функции будет готов вернуть результат, выполнение продолжится с этой строки дальше.

Пример:

let response = await fetch('coffee.jpg');

Значение Promise, которое вернёт fetch() будет присвоено переменной response только тогда, когда оно будет доступно — парсер делает паузу на данной строке дожидаясь этого момента. Как только значение доступно, парсер переходит к следующей строке, в которой создаётся объект Blob из результата Promise. В этой строке, кстати, также используется await, потому что метод .blob() также возвращает Promise. Когда результат готов, мы возвращаем его наружу из myFetch().

Обратите внимание, когда мы вызываем myFetch(), она возвращает Promise, поэтому мы можем вызвать .then() на результате, чтобы отобразить его на экране.

К этому моменту вы наверное думаете «Это реально круто!», и вы правы — чем меньше блоков .then(), тем легче читать код.

Добавляем обработку ошибок

Чтобы обработать ошибки у нас есть несколько вариантов

Мы можем использовать синхронную try...catch структуру с async/await. Вот изменённая версия первого примера выше:

async function myFetch() {
  try {
    let response = await fetch('coffee.jpg');

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    } else {
      let myBlob = await response.blob();
      let objectURL = URL.createObjectURL(myBlob);
      let image = document.createElement('img');
      image.src = objectURL;
      document.body.appendChild(image);
    }
  } catch(e) {
    console.log(e);
  }
}

myFetch();

В блок catch() {} передаётся объект ошибки, который мы назвали e; мы можем вывести его в консоль, чтобы посмотреть детали: где и почему возникла ошибка.

Если вы хотите использовать гибридный подходы (пример выше), лучше использовать блок .catch() после блока .then() вот так:

async function myFetch() {
  let response = await fetch('coffee.jpg');
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    return await response.blob();
  }
}

myFetch().then((blob) => {
  let objectURL = URL.createObjectURL(blob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
})
.catch((e) =>
  console.log(e)
);

Так лучше, потому что блок .catch() словит ошибки как из асинхронной функции, так и из Promise. Если бы мы использовали блок try/catch, мы бы не словили ошибку, которая произошла в самой myFetch() функции.

Вы можете посмотреть оба примера на GitHub:

  • simple-fetch-async-await-try-catch.html (смотреть source code)
  • simple-fetch-async-await-promise-catch.html (смотреть source code)

Await и Promise.all()

Как вы помните, асинхронные функции построены поверх promises, поэтому они совместимы со всеми возможностями последних. Мы легко можем подождать выполнение Promise.all(), присвоить результат в переменную и все это сделать используя синхронный стиль. Опять, вернёмся к примеру, рассмотренному в предыдущей статье. Откройте пример в соседней вкладке, чтобы лучше понять разницу.

Версия с async/await (смотрите live demo и source code), сейчас выглядит так:

async function fetchAndDecode(url, type) {
  let response = await fetch(url);

  let content;

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    if(type === 'blob') {
      content = await response.blob();
    } else if(type === 'text') {
      content = await response.text();
    }

    return content;
  }

}

async function displayContent() {
  let coffee = fetchAndDecode('coffee.jpg', 'blob');
  let tea = fetchAndDecode('tea.jpg', 'blob');
  let description = fetchAndDecode('description.txt', 'text');

  let values = await Promise.all([coffee, tea, description]);

  let objectURL1 = URL.createObjectURL(values[0]);
  let objectURL2 = URL.createObjectURL(values[1]);
  let descText = values[2];

  let image1 = document.createElement('img');
  let image2 = document.createElement('img');
  image1.src = objectURL1;
  image2.src = objectURL2;
  document.body.appendChild(image1);
  document.body.appendChild(image2);

  let para = document.createElement('p');
  para.textContent = descText;
  document.body.appendChild(para);
}

displayContent()
.catch((e) =>
  console.log(e)
);

Вы видите, что мы легко изменили fetchAndDecode() функцию в асинхронный вариант. Взгляните на строку с Promise.all():

let values = await Promise.all([coffee, tea, description]);

С помощью await мы ждём массив результатов всех трёх Promises и присваиваем его в переменную values. Это асинхронный код, но он написан в синхронном стиле, за счёт чего он гораздо читабельнее.

Мы должны обернуть весь код в синхронную функцию, displayContent(), и мы не сильно сэкономили на количестве кода, но мы извлекли код блока .then(), за счёт чего наш код стал гораздо чище.

Для обработки ошибок мы добавили блок .catch() для функции displayContent(); Это позволило нам отловить ошибки в обоих функциях.

Примечание: Мы также можем использовать синхронный блок finally внутри асинхронной функции, вместо асинхронного .finally(), чтобы получить информацию о результате нашей операции — смотрите в действии в нашем live example (смотрите source code).

Недостатки async/await

Асинхронные функции с async/await бывают очень удобными, но есть несколько замечаний, о которых полезно знать.

Async/await позволяет вам писать код в синхронном стиле. Ключевое слово await блокирует приостанавливает выполнение ptomise-based функции до того момента, пока promise примет статус fulfilled. Это не блокирует код за пределами вашей асинхронной функции, тем не менее важно помнить, что внутри асинхронной функции поток выполнения блокируется.

ваш код может стать медленнее за счёт большого количества awaited promises, которые идут один за другим. Каждый await должен дождаться выполнения предыдущего, тогда как на самом деле мы хотим, чтобы наши Promises выполнялись одновременно, как если бы мы не использовали async/await.

Есть подход, который позволяет обойти эту проблему — сохранить все выполняющиеся Promises в переменные, а уже после этого дожидаться (awaiting) их результата. Давайте посмотрим на несколько примеров.

Мы подготовили два примера — slow-async-await.html (см. source code) и fast-async-await.html (см. source code). Они оба начинаются с функции возвращающей promise, имитирующей асинхронность процессов при помощи вызова setTimeout():

function timeoutPromise(interval) {
  return new Promise((resolve, reject) => {
    setTimeout(function(){
      resolve("done");
    }, interval);
  });
};

Далее в каждом примере есть асинхронная функция timeTest() ожидающая три вызова timeoutPromise():

async function timeTest() {
  ...
}

В каждом примере функция записывает время начала исполнения и сколько времени понадобилось на исполнение timeTest() промисов, вычитая время в момент запуска функции из времени в момент разрешения промисов:

let startTime = Date.now();
timeTest().then(() => {
  let finishTime = Date.now();
  let timeTaken = finishTime - startTime;
  alert("Time taken in milliseconds: " + timeTaken);
})

Далее представлена асинхронная функция timeTest() различная для каждого из примеров.

В случае с медленным примером slow-async-await.html, timeTest() выглядит:

async function timeTest() {
  await timeoutPromise(3000);
  await timeoutPromise(3000);
  await timeoutPromise(3000);
}

Здесь мы просто ждём все три timeoutPromise() напрямую, блокируя выполнение на данного блока на 3 секунды при каждом вызове. Все последующие вызовы вынуждены ждать пока разрешится предыдущий. Если вы запустите первый пример (slow-async-await.html) вы увидите alert сообщающий время выполнения около 9 секунд.

Во втором fast-async-await.html примере, функция timeTest() выглядит как:

async function timeTest() {
  const timeoutPromise1 = timeoutPromise(3000);
  const timeoutPromise2 = timeoutPromise(3000);
  const timeoutPromise3 = timeoutPromise(3000);

  await timeoutPromise1;
  await timeoutPromise2;
  await timeoutPromise3;
}

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

Ниже мы ожидаем разрешения промисов из объекта в результат, так как они были запущенны одновременно, блокируя поток, то и разрешатся одновременно. Если вы запустите второй пример вы увидите alert, сообщающий время выполнения около 3 секунд.

Важно не забывать о быстродействии применяя await, проверяйте количество блокировок.

Async/await class methods

В качестве последнего замечания, вы можете использовать async перед методами классов или объектов, вынуждая их возвращать promises. А также await внутри методов объявленных таким образом. Посмотрите на пример ES class code, который мы наблюдали в статье object-oriented JavaScript, и сравните его с модифицированной (асинхронной) async версией ниже:

class Person {
  constructor(first, last, age, gender, interests) {
    this.name = {
      first,
      last
    };
    this.age = age;
    this.gender = gender;
    this.interests = interests;
  }

  async greeting() {
    return await Promise.resolve(`Hi! I'm ${this.name.first}`);
  };

  farewell() {
    console.log(`${this.name.first} has left the building. Bye for now!`);
  };
}

let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);

Первый метод класса теперь можно использовать таким образом:

han.greeting().then(console.log);

Browser support (Поддержка браузерами)

One consideration when deciding whether to use async/await is support for older browsers. They are available in modern versions of most browsers, the same as promises; the main support problems come with Internet Explorer and Opera Mini.

If you want to use async/await but are concerned about older browser support, you could consider using the BabelJS library — this allows you to write your applications using the latest JavaScript and let Babel figure out what changes if any are needed for your user’s browsers. On encountering a browser that does not support async/await, Babel’s polyfill can automatically provide fallbacks that work in older browsers.

Заключение

Вот пожалуй и все — async/await позволяют писать асинхронный код, который легче читать и поддерживать. Даже учитывая, что поддержка со стороны браузеров несколько хуже, чем у promise.then, всё же стоит обратить на него внимание.

  • Назад
  • Обзор: Asynchronous
  • Далее (en-US)

In this module

  1. Documentation
  2. Error handling with async/await and promises, n² ways to shoot yourself in the foot

Table of Contents

  • Overview of async exception handling
  • Handling errors in promises locally

    • Thrown errors
    • Rejected promises
    • Errors thrown in a different call stack
  • Handling errors in promises globally
  • Logging errors in promises with CatchJS

Browsers now have native support for doing asynchronous calls via async/await. This is nice. It is essentially syntax support for promises.

Unfortunately, the error handling story for all this not so nice. The mechanisms are interwoven, and don’t always interact with each other in a clean way.
The following table summarizes this, and we’ll dig into it further in the text below.

Overview of async exception handling

If I cause an error with 🢂
can I catch it with 🢃?
throw new Error() reject()
try {} catch {} Yes, but if the throw happens in a Promise it must have been awaited with the await syntax, and resolve must not have been called before the throw. Will not catch errors thrown in another call stack via a setTimeout() or setInterval() callback. Yes, but only if the function was called with the await syntax, and only if resolve() has not been called for the promise already.
promise.catch(e => {}) Yes, unless resolve() was called earlier or the error happened in an asynchronous callback function, for example, a function passed to setTimeout(). Yes, unless resolve() was called earlier.
window.onunhandledrejection Yes, but not until script execution has completed, your call stack is unwound, and control is yielded back to the runtime, and none of the other mechanisms have dealt with error up until then.
window.onerror Not if the error was thrown in a Promise. No.

Real footage of the async error delegation mechanism

How did we end up with this? Well, when adding new features to a system, if every feature number n has to interact
with all of the existing n-1 features, you get an O(n²) growth in feature-feature interactions. So for a linear
growth in features, you get a quadratic growth in complexity. This actually explains why most big software projects
fail, and why disentangling features is so important. It’s also what has happened to async error handling. We
started with simple callback functions, and found that it was a mess. Then we fixed that mess with Promises,
and found that that solution also was a bit of a mess. Then we fixed that mess with async/await.

So let’s dig into the current mess.

Handling errors in promises locally

Promises, they break before they’re made

Sometimes, sometimes

— The Strokes, in a post to the WHATWG mailing list

Thrown errors

When an error is thrown in an async function, you can catch it with a try {} catch {}. So this works as you’d expect:

async function fails() {
    throw Error();
}

async function myFunc() {
    try {
        await fails();
    } catch (e) {
        console.log("that failed", e); 
    }
}

This is syntax sugar for what you might have been doing with promises earlier:

fails().catch(e => {
    console.log("That also failed", e); 
});

In fact, anywhere you use the keyword await, you can remove await and
do the traditional .then() and .catch() calls. This is because the async
keyword implicitly creates a Promise for its function.

The only difference between these two is that the callback for catch() has
it’s own execution context, i.e. variable scope works like you’d expect it to.

Rejected promises

So with Promises, it turns out you have another way of throwing errors, other than using throw, namely by calling reject():

function fails2() {
    return new Promise((resolve, reject) => {
        reject(new Error());
    });
}

async function myFunc2() {
    try {
        await fails2();
    } catch (e) {
        console.log("that failed", e); 
    }
}

Errors passed to reject() can be caught with both try {} catch {} and with the .catch() method.
So you have two ways to throw errors, and two ways to catch errors. This is more complex than we’d like, but at least
each way of catching errors will catch both ways of throwing them, so the complexity here isn’t fully as bad as it could have been.

Errors thrown in a different call stack

There’s more troubly to be had though. If you’re creating Promise yourself,
chances are you’re using either a setTimeout() or a setInterval(),
or in some way calling a callback function when some operation is done. These callbacks will be
called from a different call stack, which means that thrown errors will propagate to somewhere
that is not your code.

Consider this example:

function fails3() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            throw new Error();
        }, 100);
    });
}

async function myFunc3() {
    try {
        await fails3();
    } catch (e) {
        console.log("that failed", e); //<-- never gets called
    }
}

The error produced here is never caught by the try {} catch {}, because it is thrown on a
different call stack. Using the .catch(() => {}) method would have the same problem.

The way to have an error propagate across such callbacks is to use the reject() function, like so:

function fails4() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            reject(new Error());
        }, 100);
    });
}

async function myFunc4() {
    try {
        await fails4();
    } catch (e) {
        console.log("that failed", e); //<-- this gets called
    }
}

This is presumably the main reason why the reject/resolve paradigm was introduced in the first place.

Sidenote: Why reject/resolve kind of sucks.

Calling reject(new Error()) in a promise is much like doing throw Error(),
except for a major difference: It’s just a function call, so it doesn’t break the execution flow
like throw does. This means you can write paradoxical code that both rejects
and resolves, like this:

function schrödinger() {
    return new Promise((resolve, reject) => {
        reject(new Error());
        resolve("great success");
    });
}

Here both reject() and resolve() will be called. So which will win?
The answer is whichever function was called first.

Now look at this weirdo:

function schrödinger2() {
    return new Promise((resolve, reject) => {
        throw resolve("huh"); //<-- this throw is executed
    });
}
async function callAsync() {
    try {
        await schrödinger2();
    } catch (e) {
        console.log("caught error", e); //<-- yet, this is never reached
    }
}

Here the promise has a single line of code, a throw statement. Yet, the try {} catch {}
is never triggered. This is because resolve was called, and the rule still is that whatever was called
first is what wins. So the throw is executed, but it is silently swallowed by the runtime. This
is bound to cause endless confusion.

These problems happen because resolve() and reject() are near duplicates
of return and throw.
I’ll claim that the only reason we have reject/resolve is to be able to move errors across call stack boundaries.
But it’s a mediocre fix for that, for several reasons. It only moves the errors you expect, so e.g.
an unexpected NullReferenceException will not be moved across boundaries unless you explicitly call
reject() with it yourself. Also, the fact that it duplicates core language features
causes a lot of problems, as seen above.

There’s a cleaner design for this. C# has had async/await since before
people started talking about it in JavaScript. There, exceptions thrown in the async callbacks are
caught, and then rethrown such that they propagate to the site that is awaiting the async operation.
JavaScript could implement this by providing substitutes for setTimeout and setInterval with new semantics for errors,
and we could ditch this resolve/reject stuff in favor of return/throw.
This would also cut down the Promises spec by 90%.

Handling errors in promises globally

So we know how to catch errors with try {} catch {} and similar mechanisms. What about
when you want to set up a global catch-all handler for all unhandled errors, for example to log these
errors to a server?

Well, how do you even tell if an error in a promise is unhandled?
When dealing with promises, you have no way of knowing if an error will be handled some time in the future.
The promise might call reject(), and some code might come along 10 minutes later and
call .catch(() => {}) on that promise, in which case the error will be handled.
For this reason, the global error handler in Promise libraries like Q and Bluebird has been
named onPossiblyUnhandledRejection, which is a fitting name. In native Promises,
this function is called onunhandledrejection, but they still can only tell if a
rejection has been unhandled so far. So onunhandledrejection is only triggered
when the currently running script has completed and control has been yielded back to the runtime,
if nothing else has caught the error in the meantime.

You can set up your global handler for async exceptions and rejections like this:

window.onunhandledrejection = function(evt) { /*Your code*/ }

or:

window.addEventListener("unhandledrejection", function(evt) { /*Your code*/ })

Here evt is an object of type PromiseRejectionEvent.
evt.promise is the promise that was rejected, and evt.reason holds whatever object was passed to the reject() function.

This is all nice and dandy, except for this:

No one except Chrome implement it (well, Chrome, and Chromium based browsers).
It is coming to Firefox, and presumably to Safari and Edge as well. But not yet.
To make matters worse, there is no good work around for these browsers, other than
not using native Promises, and relying on a library like Q or Bluebird instead.
Hopefully native support will arrive for these browsers soon.

Summer 2019 update: unhandledrejection is now supported by Chrome, FireFox, Edge and Safari.

Logging errors in promises with CatchJS

CatchJS instruments the browser with a global error handler, in order to track uncaught errors that occur. Deployment is simply done by dropping in a script file.

<script src="https://cdn.catchjs.com/catch.js"></script>

With this, uncaught errors get logged, along with various telemetry, which can include screenshots and click trails.

CatchJS does not attach it self to the onunhandledrejection handler. If you want this, you can set up such forwarding manually.

window.onunhandledrejection = function(evt) {
    console.error(evt.reason);
}

CatchJS will instrument console.error, so these errors will be logged to your remote persistent log, as well as to the developers console.


There exists a unique syntax that can enhance your performance with promises. It’s async/await: a surprisingly easy and comfortable means to deal with promises.

The async function declaration specifies an asynchronous function, which can return an AsyncFunction object. Async functions perform in a separate order than the rest of the code through the event loop and return a Promise as its result. Yet, the structure and the syntax of the code look similar to standard synchronous functions.

As a rule, the keyword async is placed before a function, like here:

async function fn() {
  return 1;
}

Putting it before a function means that a function always returns a promise.

Let’s check out a case, where the function automatically returns a resolved promise with the result of 1:

w3docs logo
Javascript async

async function fn() {
return 1;
}
fn().then(console.log); // 1

You could explicitly return a promise that would be the same. Here is an example:

w3docs logo
Javascript async function returned a promise

async function fn() {
return Promise.resolve(1);
}
fn().then(console.log); // 1

So, let’s assume that async returns a promise, wrapping non-promises in it. It can make your tasks much more manageable. Moreover, there exists another keyword, await, which works inside the async functions.

The await keyword makes JavaScript wait till the promise settles and returns the result.

The syntax looks like this:

//  only works inside async functions
let val = await promise;

Let’s explore an example with a promise resolving in a second:

w3docs logo
Javascript async function a promise resolving in a second

async function fn() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve(«Done!»), 1000)
});
let res = await promise; // wait until the promise resolves (*)
console.log(res); // result: «Done!»
}
fn();

The function execution is suspended at the line (*) and resumed when the promise settles with res transforming into its result. So, in a second, the code shows “done”.

await doesn’t cost any CPU resources, as the engine is capable of doing other jobs in the meantime.

So, await can be considered a more elegant way to get the promise result than promise.then.

Note that you can’t use await in regular functions: a syntax error will occur.

For instance:

function fn() {
  let promise = Promise.resolve(1);
  let result = await promise; // Syntax error
}

The error, in the example above, will occur if you don’t put async before a function. So, await always works inside an async function.

Also, consider that await doesn’t operate in the top-level code.

See the following example:

// syntax error in top-level code
let response = await fetch('/promiseChaining/user.json');
let user = await response.json();

We can assume that the code above will not work. But, you have the option of wrapping it into an anonymous async function, as follows:

(async() => {
  let response = await fetch('/promiseChaining/user.json');
  let user = await response.json();
  ...
})();

await allows using thenable objects, like promise.then.

Here the primary idea is that a third-party object might not be a promise, but promise-compatible, in case it supports .then, it’s enough for using with await.

This is illustrated in the example below:

w3docs logo
Javascript async/await thenable

class Thenable {
constructor(number) {
this.number = number;
}
then(resolve, reject) {
console.log(resolve);
// resolve with this.num+2 after 2000ms
setTimeout(() => resolve(this.number + 2), 2000); // (*)
}
};
async function fn() {
// waits for 2 second, then result becomes 4
let result = await new Thenable(2);
console.log(result);
}
fn();

In the event of getting a non-promise object with .then, await calls that method and provides the built-in functions resolve and reject as arguments. Afterward, await waits till one of them is called, then proceeds with the results.

If you want to declare an async class method, you should prepend it with async, as follows:

w3docs logo
Javascript async/await

class Supplier{
async wait() {
return await Promise.resolve(1);
}
}
new Supplier()
.wait()
.then(console.log); // 1

The idea is the same: ensuring that the returned value is a promise and enabling await.

The await promise returns a result when a promise resolves typically.
But, in the event of a rejection, it throws the error, just if it were a throw statement at that line.

The following two codes are equivalent:

async function fn() {
  await Promise.reject(new Error("Error!!"));
}
async function fn() {
  throw new Error("Error!!");
}

In real situations, the promise takes some time before the rejection. In such a case, there might be a delay before await throws an error.

It is possible to catch that error using try..catch like a regular throw.

One example is the following:

w3docs logo
Javascript async/await try..catch

async function fn() {
try {
let response = await fetch(‘http://noSuchUrl’);
} catch (err) {
console.log(err); // TypeError: failed to fetch
}
}
fn();

If an error occurs, the control jumps to the catch block. You can also wrap multiple lines, like here:

w3docs logo
Javascript async/await try..catch

async function fn() {
try {
let response = await fetch(‘/noUserHere’);
let user = await response.json();
} catch (err) {
// catches errors both in fetch and response.json
console.log(err);
}
}
fn();

In case, you don’t have try..catch, the promise created by the call of the async function fn() is rejected. So, .catch can be appended for handling it:

w3docs logo
Javascript async/await

async function fn() {
let response = await fetch(‘http://noSuchUrl’);
}
// fn() becomes a rejected promise
fn().catch(console.log); // TypeError: failed to fetch (*)

Another important note: if you forget to add .catch, you will get an unhandled promise error. Such kinds of errors can be handled with unhandledrejection event handler. We have already explored it in the chapter Error handling with promises.

While using async/await, you may rarely need to apply .then, as await handles the waiting for you. Moreover, it is possible to use try..catch instead of .catch. Usually, it’s handier.

But, as it was already mentioned, at the top level of the code, when you are outside any async function, you will syntactically not be able to use await. Hence, it’s a common practice to add .then/catch for handling the final result or falling-through an error, as in the line (*) of the case above.

Async/await, operates efficiently with Promise.all.

For instance, when you need to wait for multiple promises, you can easily wrap then into Promise.all and then await, like here:

// wait for the array of results
let results = await Promise.all([
  fetch(urlOne),
  fetch(urlTwo),
  ...
]);

If an error occurs, it propagates normally: from the failed promise to Promise.all, then becomes an exception, which you can catch implementing try..catch around the call.

In general, we can distinguish the following two effects of the async keyword:

  1. Making it always return a promise.
  2. Allowing await to be used inside it.

The keyword await that is put before a promise makes JavaScript wait till the promise settles and then acts as follows:

  1. In case an error occurs, the exception is generated: the same as if the throw error.
  2. In other cases, it will return the result.

Together async and await provide an excellent framework for writing asynchronous code, easy to write, and read.

Another prominent advantage: you will rarely need to write promise.then/catch with async/await. Also, Promise.all is there to help you whenever you are waiting for multiple tasks simultaneously.

Понравилась статья? Поделить с друзьями:
  • Aswbidsdriver sys синий экран как исправить
  • Aswarpot sys ошибка
  • Asustpcenter exe ошибка приложения
  • Asustpcenter exe application error
  • Asustek easy flash utility check system error