Async function return error

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

Время прочтения
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».

Объявление async function определяет асинхронную функцию, которая возвращает объект AsyncFunction.

Вы также можете определить async-функции, используя выражение async function.

Синтаксис

async function name([param[, param[, ... param]]]) {
   statements
}
name

Имя функции.

param

Имя аргумента, который будет передан в функцию.

statements

Выражение, содержащее тело функции.

Описание

После вызова функция async возвращает Promise. Когда результат был получен, Promise завершается, возвращая полученное значение. Когда функция async выбрасывает исключение, Promise ответит отказом с выброшенным (throws) значением.

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

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

Примечание: Цель функций async/await упростить использование promises синхронно и воспроизвести некоторое действие над группой Promises. Точно так же как Promises подобны структурированным колбэкам, async/await подобна комбинации генераторов и promises.

Примеры

Простой пример

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function add1(x) {
  const a = await resolveAfter2Seconds(20);
  const b = await resolveAfter2Seconds(30);
  return x + a + b;
}

add1(10).then(v => {
  console.log(v);  // prints 60 after 4 seconds.
});

async function add2(x) {
  const a = resolveAfter2Seconds(20);
  const b = resolveAfter2Seconds(30);
  return x + await a + await b;
}

add2(10).then(v => {
  console.log(v);  // prints 60 after 2 seconds.
});

Предупреждение: #### Не путайте await и Promise.allФункция add1 приостанавливается на 2 секунды для первого await и ещё на 2 для второго. Второй таймер создаётся только после срабатывания первого. В функции add2 создаются оба и оба же переходят в состояние await. В результате функция add2 завершится скорее через две, чем через четыре секунды, поскольку таймеры работают одновременно. Однако запускаются они всё же не параллельно, а друг за другом — такая конструкция не означает автоматического использования Promise.all. Если два или более Promise должны разрешаться параллельно, следует использовать Promise.all.

Когда функция async выбрасывает исключение

async function throwsValue() {
    throw new Error('oops');
}
throwsValue()
    .then((resolve) => {
            console.log("resolve:" + resolve);
        },
        (reject) => {
            console.log("reject:" + reject);
        });
//prints "reject:Error: oops"
//or
throwsValue()
    .then((resolve) => {
        console.log("resolve:" + resolve);
    })
    .catch((reject) => {
        console.log("reject:" + reject);
    });
//prints "reject:Error: oops"

Перепись цепочки promise с использованием функции async

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

function getProcessedData(url) {
  return downloadData(url) // returns a promise
    .catch(e => {
      return downloadFallbackData(url) // returns a promise
    })
    .then(v => {
      return processDataInWorker(v); // returns a promise
    });
}

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

async function getProcessedData(url) {
  let v;
  try {
    v = await downloadData(url);
  } catch(e) {
    v = await downloadFallbackData(url);
  }
  return processDataInWorker(v);
}

Заметьте, что пример выше не содержит await на return, потому что возвращаемое значение функции async неявно обёрнуто в Promise.resolve.

Спецификации

Specification
ECMAScript Language Specification
# sec-async-function-definitions

Поддержка браузерами

BCD tables only load in the browser

Смотрите также

Sobio Darlington

This article is intended to suggest a better way to handle errors when using async/await syntax. Prior knowledge of how promises work is important.

From Callback Hell to Promises

Callback Hell, also known as Pyramid of Doom, is an anti-pattern seen in code of programmers who are not wise in the ways of asynchronous programming. — Colin Toh

Callback hell makes your code drift to the right instead of downward due to multiple nesting of callback functions.

I wont go into details of what callback hell is, but I’ll give an example of how it looks.

User profile example 1

// Code that reads from left to right 
// instead of top to bottom

let user;
let friendsOfUser;

getUser(userId, function(data) {
  user = data;

  getFriendsOfUser(userId, function(friends) {
    friendsOfUser = friends;

    getUsersPosts(userId, function(posts) {
      showUserProfilePage(user, friendsOfUser, posts, function() {
        // Do something here

      });
    });
  });
});

Enter fullscreen mode

Exit fullscreen mode

Promises

Promises were introduced to the Javascript(ES6) language to handle asynchronous operations better without it turning into a callback hell.

The example below use promises to solve callback hell by using multiple chained .then calls instead of nesting callbacks.

User profile example 2

// A solution with promises

let user;
let friendsOfUser;

getUser().then(data => {
  user = data;

  return getFriendsOfUser(userId);
}).then(friends => {
  friendsOfUser = friends;

  return getUsersPosts(userId);
}).then(posts => {
  showUserProfilePage(user, friendsOfUser, posts);
}).catch(e => console.log(e));

Enter fullscreen mode

Exit fullscreen mode

The solution with promise looks cleaner and more readable.

Promises with with async/await

Async/await is a special syntax to work with promises in a more concise way.
Adding async before any function turns the function into a promise.

All async functions return promises.

Example

// Arithmetic addition function
async function add(a, b) {
  return a + b;
}

// Usage: 
add(1, 3).then(result => console.log(result));

// Prints: 4

Enter fullscreen mode

Exit fullscreen mode

Making the User profile example 2 look even better using async/await

User profile example 3

async function userProfile() {
  let user = await getUser();
  let friendsOfUser = await getFriendsOfUser(userId);
  let posts = await getUsersPosts(userId);

  showUserProfilePage(user, friendsOfUser, posts);
}

Enter fullscreen mode

Exit fullscreen mode

Wait! there’s a problem

If theres a promise rejection in any of the request in User profile example 3, Unhandled promise rejection exception will be thrown.

Before now Promise rejections didn’t throw errors. Promises with unhandled rejections used to fail silently, which could make debugging a nightmare.

Thank goodness promises now throws when rejected.

  • Google Chrome throws: VM664:1 Uncaught (in promise) Error

  • Node will throw something like: (node:4796) UnhandledPromiseRejectionWarning: Unhandled promise rejection (r ejection id: 1): Error: spawn cmd ENOENT
    [1] (node:4796) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

No promise should be left uncaught. — Javascript

Notice the .catch method in User profile example 2.
Without the .catch block Javascript will throw Unhandled promise rejection error when a promise is rejected.

Solving this issue in User profile example 3 is easy. Unhandled promise rejection error can be prevented by wrapping await operations in a try…catch block:

User profile example 4

async function userProfile() {
  try {
    let user = await getUser();
    let friendsOfUser = await getFriendsOfUser(userId);
    let posts = await getUsersPosts(userId);

    showUserProfilePage(user, friendsOfUser, posts);
  } catch(e) {
    console.log(e);
  }
}

Enter fullscreen mode

Exit fullscreen mode

Problem solved!

…But error handling could be improved

How do you know with error is from which async request?

We can call a .catch method on the async requests to handle errors.

User profile example 5

let user = await getUser().catch(e => console.log('Error: ', e.message));

let friendsOfUser = await getFriendsOfUser(userId).catch(e => console.log('Error: ', e.message));

let posts = await getUsersPosts(userId).catch(e => console.log('Error: ', e.message));

showUserProfilePage(user, friendsOfUser, posts);

Enter fullscreen mode

Exit fullscreen mode

The solution above will handle individual errors from the requests, but its a mix of patterns. There should be a cleaner way to use async/await without using .catch method (Well, you could if you don’t mind).

Here’s my solution to a better async/await error handling

User profile example 6

/**
 * @description ### Returns Go / Lua like responses(data, err) 
 * when used with await
 *
 * - Example response [ data, undefined ]
 * - Example response [ undefined, Error ]
 *
 *
 * When used with Promise.all([req1, req2, req3])
 * - Example response [ [data1, data2, data3], undefined ]
 * - Example response [ undefined, Error ]
 *
 *
 * When used with Promise.race([req1, req2, req3])
 * - Example response [ data, undefined ]
 * - Example response [ undefined, Error ]
 *
 * @param {Promise} promise
 * @returns {Promise} [ data, undefined ]
 * @returns {Promise} [ undefined, Error ]
 */
const handle = (promise) => {
  return promise
    .then(data => ([data, undefined]))
    .catch(error => Promise.resolve([undefined, error]));
}

async function userProfile() {
  let [user, userErr] = await handle(getUser());

  if(userErr) throw new Error('Could not fetch user details');

  let [friendsOfUser, friendErr] = await handle(
    getFriendsOfUser(userId)
  );

  if(friendErr) throw new Error('Could not fetch user's friends');

  let [posts, postErr] = await handle(getUsersPosts(userId));

  if(postErr) throw new Error('Could not fetch user's posts');

  showUserProfilePage(user, friendsOfUser, posts);
}

Enter fullscreen mode

Exit fullscreen mode

Using the handle utility function, we are able to avoid Unhandled promise rejection error and also handle error granularly.

Explanation

The handle utility function takes a promise as an argument and always resolves it, returning an array with [data|undefined, Error|undefined].

  • If the promise passed to the handle function resolves it returns [data, undefined];
  • If it was rejected, the handle function still resolves it and returns [undefined, Error]

Similar solutions

  • Easier Error Handling Using Async/Await — Jesse Warden
  • NPM Package — await-to-js

Conclusion

Async/await has a clean syntax, but you still have to handle thrown exceptions in async functions.

Handling error with .catch in promise .then chain can be difficult unless you implement custom error classes.

Using the handle utility function, we are able to avoid Unhandled promise rejection error and also handle error granularly.

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

Ниже приведён пример асинхронного чтения файла на Node.js:

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

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

  • Выполняется запрос в БД на некого пользователя Arfat. Нужно считать его поле profile_img_url и загрузить соответствующее изображение с сервера someServer.ru.
  • После загрузки изображения необходимо его конвертировать, допустим из PNG в JPEG.
  • В случае успешной конвертации нужно отправить письмо на почту пользователя.
  • Это событие нужно занести в файл transformations.log и указать дату.
queryDatabase({ username: 'Arfat'}, (err, user) => {
  // Обработка ошибок при запросе в БД
  const image_url = user.profile_img_url;
  getImageByURL('someServer.com/q=${image_url}', (err, image) => {
    // Обработка ошибок получения изображения
    transformImage(image, (err, transformedImage) => {
      // Обработка ошибок конвертирования 
      sendEmail(user.email, (err) => {
        // Обработка ошибок отсылки по почте
        logTaskInFile('Конвертирование файла и отсылка по почте', (err) 
          // Обработка ошибок лога
        })
      })
    })
  })
})

Обратите внимание на вложенность обратных вызовов и пирамиду из }) в конце. Подобные случаи принято называть Callback Hell или Pyramid of Doom. Вот основные недостатки:

  • Такой код сложно читать.
  • В таком коде сложно обрабатывать ошибки и одновременно сохранять его «качество».

Для решения этой проблемы в JavaScript были придуманы промисы (англ. promises). Теперь глубокую вложенность коллбэков можно заменить ключевым словом then:

queryDatabase({ username: 'Arfat'})
  .then((user) => {
    const image_url = user.profile_img_url;
    return getImageByURL('someServer.com/q=${image_url}') 
      .then(image => transformImage(image))
      .then(() => sendEmail(user.email))
})
.then(() => logTaskInFile('...'))
.catch(() => handleErrors()) // Обработка ошибок

Код стал читаться сверху вниз, а не слева направо, как это было в случае с обратными вызовами. Это плюс к читаемости. Однако и у промисов есть свои проблемы:

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

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

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

Стоит ли говорить, что в решении этой задачи нельзя использовать конструкцию async/await либо .sort функцию? Решение будет в конце.


Добавление async-функций в ES2017 (ES8) сделало работу с промисами легче.

  • Важно отметить, что async-функции работают поверх промисов.
  • Эти функции не являются принципиально другими концепциями.
  • Async-функции были задуманы как альтернатива коду, использующему промисы.
  • Используя конструкцию async/await, можно полностью избежать использование цепочек промисов.
  • С помощью async-функций возможно организовать работу с асинхронным кодом в синхронном стиле.

Как видите, знание промисов всё же необходимо для понимания работы async/await.

Синтаксис

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

// В объявлении функции
async function myFn() {
  // await ...
}

// В стрелочной функции
const myFn = async () => {
  // await ...
}

function myFn() {
  // await fn(); (синтаксическая ошибка, т. к. нет async)
}

Обратите внимание, что async вставляется в начале объявления функции, а в случае стрелочной функции — между знаком = и скобками.

Async-функции могут быть помещены в объект в качестве методов или же просто использоваться в объявлении класса.

// В качестве метода объекта
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}

// В самом классе
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

Примечание Конструкторы класса и геттеры/сеттеры не могут быть асинхронными.

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

Async-функции похожи на обычные функции в JavaScript, за исключением нескольких вещей:

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, код вручную возвращает промис.

Тело асинхронной функции всегда обёртывается в новый промис

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

// В случае примитивного типа значения
const p = Promise.resolve('hello')
p instanceof Promise; 
// true

// p возвращается как есть

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 и потом завершается.

// Функция задержки
// с возвращением случайного числа
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;
}

// Вызов fn
fn().then(console.log);

Как работает fn функция?

  1. После вызова fn функции первая строка конвертируется из const a = await 9; в const a = await Promise.resolve(9);.
  2. После использования await, выполнение функции приостанавливается, пока a не получит своё значение (в данном случае это 9).
  3. delayAndGetRandom(1000) приостанавливает выполнение fn функции, пока не завершится сама (после 1 секунды). Это, фактически, можно назвать остановкой fn функции на 1 секунду.
  4. Также delayAndGetRandom(1000) через resolve возвращает случайное значение, которое присваивается переменной b.
  5. Случай с переменной c идентичен случаю переменной a. После этого опять происходит пауза на 1 секунду, но теперь delayAndGetRandom(1000) ничего не возвращает, т. к. этого не требуется.
  6. Под конец эти значения считаются по формуле a + b * c. Результат обёртывается в промис с помощью Promise.resolve и возвращается функцией.

Примечание Если такие паузы напоминают вам генераторы в ES6, то на это есть свои причины.

Решение задачи

Вот решение задачи, поставленной в начале статьи, с использованием async/await.

async function finishMyTask() {
  try {
    const user = await queryDatabase({ username: 'Arfat' }); 
    const image_url = user.profile_img_url;
    const image = await getImageByURL('someServer.com/q=${image_url}'); 
    const transformedlmage = await transformImage(image); 
    await sendEmail(user.email); 
    await logTaskInFile(' ... ');
  } catch(err) {
    // Обработка всех ошибок
  }
}

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

Вот то решение с выводом чисел. Тут есть два способа:

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));

// Решение #1 (с использованием цикла for)
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);
});

// Решение #2 (с использованием рекурсии)

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() {
  // Ждём секунду
  await new Promise(res => setTimeout(res, 1000));
// Реджектим в 50% случае
  if (Math.random() > 0.5) {
    throw new Error('Простите, число больше, чем нужно.')
  }

return 'Число подошло';
}

canRejectOrReturn() — это асинхронная функция, которая будет удачно завершатся с 'Число подошло', либо неудачно завершаться с Error('Простите, число больше, чем нужно.').

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'Ошибка обработана';
  }
}

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

Вот другой пример:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'Ошибка обработана';
  }
}

Обратите внимание, что в коде выше из foo возвращается (без ожидания) canRejectOrReturn. foo завершится либо с 'число подошло', либо с реджектом Простите, число больше, чем нужно.‘). Блок catch никогда не будет исполняться.

Это происходит из-за того, что foo возвращает промис, который передан от canRejectOrReturn. Следовательно, решение функции foo становится решением canRejectOrReturn. Такой код можно представить всего в двух строках:

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

Вот что получится, если использовать await и return разом:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'Ошибка обработана';
  }
}

В коде выше foo будет удачно завершаться и с  'число подошло', и с 'Ошибка обработана'. В таком коде реджектов не будет. Но в отличие от одного из примеров выше, 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 'Обработка';
  }
}

Обратите внимание, здесь не используется ни await, ни return. Функция foo всегда будет завершаться с undefined (без задержки в 1 секунду). Тем не менее, промис будет выполняться. Если промис будет выдавать ошибку либо реджект, то будет вызываться UnhandledPromiseRejectionWarning.

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

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

const url = 'https://api.github.com/users';

// Получает количество открытых репозиториев
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 будет содержать число — количество репозиториев. Но как было сказано ранее, все async-функции возвращают промисы. Следовательно, 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 Iterators.

Перевод статьи «Deeply Understanding JavaScript Async and Await with Examples»

Error handling in async/await causes a lot of confusion. There are
numerous patterns for handling errors in async functions, and even experienced developers sometimes
get it wrong.

Suppose you have an async function run(). In this article,
I’ll describe 3 different patterns
for handling errors in run(): try/catch, Golang-style,
and catch() on the function call. I’ll also explain why you
rarely need anything but catch() with async functions.

try/catch

When you’re first getting started with async/await, it is tempting
to use try/catch around every async operation. That’s because
if you await on a promise that rejects, JavaScript throws a
catchable error.

run();

async function run() {
  try {
    await Promise.reject(new Error('Oops!'));
  } catch (error) {
    error.message; // "Oops!"
  }
}

try/catch also handles synchronous errors.

run();

async function run() {
  const v = null;
  try {
    await Promise.resolve('foo');
    v.thisWillThrow;
  } catch (error) {
    // "TypeError: Cannot read property 'thisWillThrow' of null"
    error.message;
  }
}

So all you need to do is wrap all your logic in a try/catch,
right? Not quite. The below code will result in an
unhandled promise rejection. The await keyword converts promise rejections to
catchable errors, but return does not.

run();

async function run() {
  try {
    // Note that this is a `return`, not `await`
    return Promise.reject(new Error('Oops!'));
  } catch (error) {
    // Will **not** run
  }
}

You could work around this limitation using return await.
However, it is easy to forget return await.

Another disadvantage is that try/catch is hard to compose.
Once you realize that try/catch handles sync and async errors,
it is tempting to wrap all your async logic in one try/catch,
as shown below.

Golang in JS

Another common pattern is using .then() to convert a promise
that rejects into a promise that fulfills with an error. You can
then use an if (err) check like in Golang.

run();

async function throwAnError() {
  throw new Error('Oops!');
}

async function noError() {
  return 42;
}

async function run() {
  // The `.then(() => null, err => err)` pattern gives you an
  // error if one occurred, or `null` otherwise
  let err = await throwAnError().then(() => null, err => err);
  if (err != null) {
    err.message; // 'Oops'
  }

  err = await noError().then(() => null, err => err);
  err; // null
}

If you need both the error and the value, you can really pretend
to write Golang in JavaScript.

run();

async function throwAnError() {
  throw new Error('Oops!');
}

async function noError() {
  return 42;
}

async function run() {
  // The `.then(v => [null, v], err => [err, null])` pattern
  // lets you use array destructuring to get both the error and
  // the result
  let [err, res] = await throwAnError().
    then(v => [null, v], err => [err, null]);
  if (err != null) {
    err.message; // 'Oops'
  }

  [err, res] = await noError().
    then(v => [null, v], err => [err, null]);
  err; // null
  res; // 42
}

This pattern can be neater syntactically because declaring a
variable in a try block with let scopes the variable to the
try block.

const getAnswer = async () => 42;

run();

async function run() {
  try {
    let val = await getAnswer();
  } catch (error) {}

  // ReferenceError: val is not defined
  val;
}

Golang-style error handling doesn’t get rid of the return quirk.
It just makes missing error checks harder, because you know that
if you don’t have if (err != null) after an async operation,
something is wrong.

There are two major disadvantages to Golang-style error handling:

  1. It is extremely repetitive. Typing if (err != null) every time you want to do something async puts you on the express lane to carpal tunnel.
  2. It doesn’t help you with synchronous errors in run().

So Golang-style error handling is a neat syntactic shortcut that
should be used sparingly. It doesn’t have much benefit over
using try/catch.

Using catch() on the Function Call

Both try/catch and Golang-style error handling have their uses,
but the best way to ensure you’ve handled all errors in your
run() function is to use run().catch(). In other words,
handle errors when calling the function as opposed to handling
each individual error.

run().
  catch(function handleError(err) {
    err.message; // Oops!
  }).
  // Handle any errors in `handleError()`. If the error handler
  // throws an error, kill the process.
  catch(err => { process.nextTick(() => { throw err; }) });

async function run() {
  await Promise.reject(new Error('Oops!'));
}

Remember that async functions always return promises. This promise
rejects if any uncaught error occurs in the function.
If your async function body returns a promise that rejects,
the returned promise will reject too.

run().
  catch(function handleError(err) {
    err.message; // Oops!
  }).
  // Handle any errors in `handleError()`. If the error handler
  // throws an error, kill the process.
  catch(err => { process.nextTick(() => { throw err; }) });

async function run() {
  // Note that this is `return`, not `await`
  return Promise.reject(new Error('Oops!'));
}

Why run().catch() as opposed to wrapping the entire run()
function body in a try/catch? For handling errors in the error
handler. What happens if the catch block in your try/catch
throws an error? The only solution is to nest a try/catch in
your catch block, in every single function. .catch() makes
handling unexpected errors in your error handler cleaner.

Takeaways

In general, errors are either expected or unexpected. In async
functions, try/catch can help you recover gracefully from
expected errors. But unexpected errors do happen, we all
occasionally end up with a surprise «TypeError: Cannot read
property ‘foo’ of null» sometimes.

You should handle unexpected errors in your async functions in
the calling function. The run() function shouldn’t be
responsible for handling every possible error, you should
instead do run().catch(handleError).

Looking to become fluent in async/await? My new ebook, Mastering Async/Await, is designed to give you an integrated understanding of
async/await fundamentals and how async/await fits in the JavaScript ecosystem in a few hours. Get your copy!

Found a typo or error? Open up a pull request! This post is
available as markdown on Github

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