Обработка ошибок для async await

Время на прочтение
8 мин

Количество просмотров 65K

Конструкция async/await появилась в стандарте ES7. Её можно считать замечательным улучшением в сфере асинхронного программирования на JavaScript. Она позволяет писать код, который выглядит как синхронный, но используется для решения асинхронных задач и не блокирует главный поток. Несмотря на то, что async/await — это отличная новая возможность языка, пользоваться ей правильно не так уж и просто. Материал, перевод которого мы публикуем сегодня, посвящён разностороннему исследованию async/await и рассказу о том, как использовать этот механизм правильно и эффективно.

image

Сильные стороны async/await

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

// async/await
async getBooksByAuthorWithAwait(authorId) {
  const books = await bookModel.fetchAll();
  return books.filter(b => b.authorId === authorId);
}
// промис
getBooksByAuthorWithPromise(authorId) {
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
}

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

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

Все основные браузеры поддерживают асинхронные функции (caniuse.com)

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

На следующем рисунке показан процесс отладки асинхронной функции. Здесь, при установке точки останова на первой инструкции функции и при выполнении команды Step Over, когда отладчик доходит до строки, в которой использовано ключевое слово await, можно заметить, как отладчик ненадолго приостанавливается, ожидая окончания работы функции bookModel.fetchAll(), а затем переходит к строке, где вызывается команда .filter()! Такой отладочный процесс выглядит куда проще, чем отладка промисов. Тут, при отладке аналогичного кода, пришлось бы устанавливать ещё одну точку останова в строке .filter().


Отладка асинхронной функции. Отладчик дождётся выполнения await-строки и перейдёт на следующую строку после завершения операции

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

getBooksByAuthorWithPromise(authorId) {
  if (!authorId) {
    return null;
  }
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
  }
}

Здесь функция getBooksByAuthorWithPromise() может, если всё нормально, вернуть промис, или, если что-то пошло не так — null. В результате, если произошла ошибка, здесь нельзя безопасно вызвать .then(). При объявлении функций с использованием ключевого слова async ошибки подобного рода невозможны.

О неправильном восприятии async/await

В некоторых публикациях конструкцию async/await сравнивают с промисами и говорят о том, что она представляет собой новое поколении эволюции асинхронного программирования на JavaScript. С этим я, при всём уважении к авторам таких публикаций, позволю себе не согласиться. Async/await — это улучшение, но это — не более чем «синтаксический сахар», появление которого не ведёт к полному изменению стиля программирования.

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

Взгляните на функции getBooksByAuthorWithAwait() и getBooksByAuthorWithPromises() из вышеприведённого примера. Обратите внимание на то, что они идентичны не только в плане функционала. У них ещё и совершенно одинаковые интерфейсы.

Всё это значит, что, если вызвать напрямую функцию getBooksByAuthorWithAwait(), она вернёт промис.

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

Подводные камни async/await

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

Хотя ключевое слово await может сделать код похожим на синхронный, пользуясь им, стоит помнить о том, что код это асинхронный, а значит, надо очень внимательно относиться к последовательным вызовом асинхронных функций.

async getBooksAndAuthor(authorId) {
  const books = await bookModel.fetchAll();
  const author = await authorModel.fetch(authorId);
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}

Этот код, с точки зрения логики, кажется правильным. Однако тут имеется серьёзная проблема. Вот как он работает.

  1. Система вызывает await bookModel.fetchAll() и ждёт завершения команды .fetchAll().
  2. После получения результата от bookModel.fetchAll() будет выполнен вызов await authorModel.fetch(authorId).

Обратите внимание на то, что вызов authorModel.fetch(authorId) не зависит от результатов вызова bookModel.fetchAll(), и, на самом деле, эти две команды можно выполнять параллельно. Однако использование await приводит к тому, что два этих вызова выполняются последовательно. Общее время последовательного выполнения этих двух команд будет больше, чем время их параллельного выполнения.

Вот правильный подход к написанию такого кода:

async getBooksAndAuthor(authorId) {
  const bookPromise = bookModel.fetchAll();
  const authorPromise = authorModel.fetch(authorId);
  const book = await bookPromise;
  const author = await authorPromise;
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}

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

async getAuthors(authorIds) {
  // Неправильный подход, вызовы будут выполнены последовательно
  // const authors = _.map(
  //   authorIds,
  //   id => await authorModel.fetch(id));
// Правильный подход
  const promises = _.map(authorIds, id => authorModel.fetch(id));
  const authors = await Promise.all(promises);
}

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

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

При использовании промисов выполнение асинхронного кода может завершиться либо так, как ожидается — тогда говорят об успешном разрешении промиса, либо с ошибкой — тогда говорят о том, что промис отклонён. Это даёт нам возможность использовать, соответственно, .then() и .catch(). Однако, обработка ошибок при использовании механизма async/await может оказаться непростым делом.

▍Конструкция try/catch

Стандартным способом для обработки ошибок при использовании async/await является конструкция try/catch. Я рекомендую пользоваться именно этим подходом. При выполнении await-вызова значение, выдаваемое при отклонении промиса, представляется в виде исключения. Вот пример:

class BookModel {
  fetchAll() {
    return new Promise((resolve, reject) => {
      window.setTimeout(() => { reject({'error': 400}) }, 1000);
    });
  }
}
// async/await
async getBooksByAuthorWithAwait(authorId) {
try {
  const books = await bookModel.fetchAll();
} catch (error) {
  console.log(error);    // { "error": 400 }
}

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

  • Можно обработать исключение и вернуть нормальное значение. Если не использовать выражение return в блоке catch для возврата того, что ожидается после выполнения асинхронной функции, это будет эквивалентно использованию команды return undefined;.
  • Можно просто передать ошибку в место вызова кода, который дал сбой, и позволить обработать её там. Можно выбросить ошибку напрямую, воспользовавшись командой наподобие throw error;, что позволит использовать функцию async getBooksByAuthorWithAwait() в цепочке промисов. То есть, вызывать её можно будет, пользуясь конструкцией getBooksByAuthorWithAwait().then(...).catch(error => ...). Кроме того, можно обернуть ошибку в объект Error, что может выглядеть как throw new Error(error). Это позволит, например, при выводе сведений об ошибке в консоль, просмотреть полный стек вызовов.
  • Ошибку можно представить в виде отклонённого промиса, выглядит это как return Promise.reject(error). В данном случае это эквивалентно команде throw error, делать так не рекомендуется.

Вот преимущества применения конструкции try/catch:

  • Подобные средства обработки ошибок существуют в программировании уже очень давно, они просты и понятны. Скажем, если у вас есть опыт программирования на других языках, вроде C++ или Java, то вы без проблем поймёте устройство try/catch в JavaScript.
  • В один блок try/catch можно помещать несколько await-вызовов, что позволяет обрабатывать все ошибки в одном месте в том случае, если нет необходимости раздельно обрабатывать ошибки на каждом шаге выполнения кода.

Надо отметить, что в механизме try/catch есть один недостаток. Так как try/catch перехватывает любые исключения, возникающие в блоке try, в обработчик catch попадут и те исключения, которые не относятся к промисам. Взгляните на этот пример.

class BookModel {
  fetchAll() {
    cb();    // обратите внимание на то, что функция `cb` не определена, что приведёт к исключению
    return fetch('/books');
  }
}
try {
  bookModel.fetchAll();
} catch(error) {
  console.log(error);  // Тут будет выдано сообщение об ошибке "cb is not defined"
}

Если выполнить этот код, можно увидеть в консоли сообщение об ошибке ReferenceError: cb is not defined. Это сообщение выведено командой console.log() из блока catch, а не самим JavaScript. В некоторых случаях такие ошибки приводят к тяжёлым последствиям. Например, если вызов bookModel.fetchAll(); запрятан глубоко в серии вызовов функций и один из вызовов «проглотит» ошибку, такую ошибку будет очень сложно обнаружить.

▍Возврат функциями двух значений

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

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

[err, user] = await to(UserModel.findById(1));

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

▍Использование .catch

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

Вспомните о том, как работает await. А именно, использование этот ключевого слова приводит к тому, что система ждёт до тех пор, пока промис не завершит свою работу. Кроме того, вспомните о том, что команда вида promise.catch() тоже возвращает промис. Всё это говорит о том, что обрабатывать ошибки асинхронных функций можно так:

// books будет равно undefined если произойдёт ошибка,
// так как обработчик catch ничего явно не возвращает
let books = await bookModel.fetchAll()
  .catch((error) => { console.log(error); });

Для этого подхода характерны две небольших проблемы:

  • Это — смесь промисов и асинхронных функций. Для того чтобы этим пользоваться, надо, как и в других подобных случаях, понимать особенности работы промисов.
  • Этот подход не отличается интуитивной понятностью, так как обработка ошибок выполняется в необычном месте.

Итоги

Конструкция async/await, которая появилась в ES7, определённо, является улучшением механизмов асинхронного программирования в JavaScript. Она способна облегчить чтение и отладку кода. Однако, для того, чтобы пользоваться async/await правильно, необходимо глубокое понимание промисов, так как async/await — это всего лишь «синтаксический сахар», в основе которого лежат промисы.

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

Уважаемые читатели! Пользуетесь ли вы конструкцией async/await в JavaScript? Если да — просим рассказать о том, как вы обрабатываете ошибки в асинхронном коде.

Here’s some code:

  import 'babel-polyfill'

  async function helloWorld () {
    throw new Error ('hi')
  }

  helloWorld()

I also went deep and tried this as well:

  import 'babel-polyfill'

  async function helloWorld () {
    throw new Error ('hi')
  }

  async function main () {
    try {
      await helloWorld()
    } catch (e) {
      throw e
    }
  }

  main()

and:

import 'babel-polyfill'

 async function helloWorld () {
   throw new Error ('hi')
 }

try {
 helloWorld()
} catch (e) {
 throw e
}

This works:

import 'babel-polyfill'

async function helloWorld () {
  throw new Error('xxx')
}

helloWorld()
.catch(console.log.bind(console))

asked Nov 6, 2015 at 8:18

ThomasReggi's user avatar

ThomasReggiThomasReggi

54.2k85 gold badges234 silver badges419 bronze badges

2

async is meant to be used with Promises. If you reject the promise, then you can catch the error, if you resolve the promise, that becomes the return value of the function.

async function helloWorld () {
  return new Promise(function(resolve, reject){
    reject('error')
  });
}


try {
    await helloWorld();
} catch (e) {
    console.log('Error occurred', e);
}

answered Nov 6, 2015 at 14:55

Ruan Mendes's user avatar

Ruan MendesRuan Mendes

89.7k31 gold badges152 silver badges215 bronze badges

5

So it’s kind of tricky, but the reason you’re not catching the error is because, at the top level, the entire script can be thought of as a synchronous function. Anything you want to catch asynchronously needs to be wrapped in an async function or using Promises.

So for instance, this will swallow errors:

async function doIt() {
  throw new Error('fail');
}

doIt();

Because it’s the same as this:

function doIt() {
  return Promise.resolve().then(function () {
    throw new Error('fail');
  });
}

doIt();

At the top level, you should always add a normal Promise-style catch() to make sure that your errors get handled:

async function doIt() {
  throw new Error('fail');
}

doIt().catch(console.error.bind(console));

In Node, there is also the global unhandledRejection event on process that you can use to catch all Promise errors.

answered Nov 6, 2015 at 15:11

nlawson's user avatar

nlawsonnlawson

11.5k4 gold badges40 silver badges48 bronze badges

To catch an error from an async function, you can await the error:

async function helloWorld () {
  //THROW AN ERROR FROM AN ASYNC FUNCTION
  throw new Error('hi')
}

async function main() {
  try {
    await helloWorld()
  } catch(e) {
    //AWAIT THE ERROR WITHIN AN ASYNC FUNCTION
    const error = await e
    console.log(error)
  }
}

main()

Alternatively, you can just await the error message:

async function main() {
  try {
    await helloWorld()
  } catch(e) {
    //AWAIT JUST THE ERROR MESSAGE
    const message = await e.message
    console.log(message)
  }
}

answered Jan 14 at 15:17

Rolazar's user avatar

Введение

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

Оказалось, что для многих концепция async/await в JS — сложная тема. И, если в большинстве случаев TypeScript не даст совершить ошибку при работе с async/await, то в этой ситуации код получается корректный (с точки зрения синтаксиса и проверки типов), а программа работает не так, как задумывалось.

Проблема

Проблема возникает при отлове ошибки в async-функциях.

Вопрос

Приведу два примера:

// --- Пример 1
async function something() {
// ...
  return await someOtherAsyncFunction();
// ...
// --- Пример 2
async function something() {
// ...
  return someOtherAsyncFunction();
// ...

Можете попробовать угадать, что не так с этим кодом?

Даже eslint помечает код в примере 1 как некорректный, за что eslint отдельное “спасибо”.

Ответ

Теперь приведу код, запустив который будет понятно, что не так со вторым примером:

async function fail() {
  throw new Error('Ultimate failure');
}

await (async () => {
  try {
    return await fail();
  } catch (error) {
    console.log('The error was caught');
  }
})();

await (async () => {
  try {
    return fail();
  } catch (error) {
    console.log('The error was not caught');
  }
})();

Как следует из кода выше, 'The error was not caught' не будет выведен, потому что во втором случае ошибка не будет поймана.

Объяснение

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

Async/await — всего лишь обёртка над промисами, которые в свою очередь являются всего лишь обёрткой над колбеками. Сумма этих двух обёрток позволяет писать асинхронный (с колбеками) код в превдо-синхронном стиле (без колбеков).

Async-функции

Объявление функции с async позволяет в теле функции использовать await.

Кроме этого, если результатом async-функции является не промис, async-функция автоматически заворачивает значение в промис (что-то вроде Promise.resolve(value)).
Пример:

async function returnNumber() {
  return 1;
}

console.log(returnNumber());

// Promise { 1 }

И, наконец, если в теле async-функции происходит выбрасывание исключения, это исключение тоже автоматически заворачивается в промис (что-то вроде Promise.reject(exception)).
Пример:

async function fail() {
  throw new Error('Ultimate failure');
}

console.log(fail());

// Promise { <rejected> Error: Ultimate failure }

Await и разворачивание промисов

Await-же занимается разворачиванием промисов, возвращённых из той функции, которая вызывалась с ключевым словом await:

async function returnNumber() {
  return 1;
}

console.log(await returnNumber());

// 1

И, если вернулся промис с reject‘ом, ошибка из этого промиса заново бросается (и затем заново возвращается через Promise.reject, если не будет поймана в try-catch):

async function fail() {
  throw new Error('Ultimate failure');
}

console.log(await fail());

// console.log ничего не выведет, будет только 'Uncaught Error: Ultimate failure'

«Кто виноват?» и «Что делать?»

В результате, если в последнем try-catch (вверх по стеку) не написать await перед потенциально-выбрасывающей-исключение-функцией, промис (с reject-ом) отправится как есть и блок try-catch не перейдёт в секцию catch. В таком случае исключение вместо обработки будет выброшено.

И если в браузере выбрашенное исключение приведёт только к красной строчке в инспекторе, то в случае сервера весь процесс ляжет (если не пользоваться совсем неприличными способами вроде process.on('uncaughtException', (error: Error) => { /* ... */ });) и в лучшем случае будет автоматически перезапущен (потеряв все данные).

Что делать? Стараться возвращать результаты асинхронных функций через await даже тогда, когда это кажется нелогичным внутри try-catch-блоков:

try {
  const result = await fail();
  return result;
} catch (error) {
  logger.log(error);
  return undefined;
}

И всегда вызывать асинхронные функции через await — даже если те возвращают пустой промис (Promise<void>):

Понятно, что на разворачивание/заворачивание промисов в таком случае будет тратиться как минимум один лишний цикл event loop, но ценой микроскопического оверхеда можно сильно стабилизировать систему на уровне подхода к коду.

Заключение

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

С тех пор как я осознал это, я использую вопрос про обработку ошибок и async/await на собеседованиях, что позволяет сразу понять: пользовался соискатель NodeJS по-настоящему или нет.

Async/await

Существует специальный синтаксис для работы с промисами, который называется «async/await». Он удивительно прост для понимания и использования.

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

Начнём с ключевого слова async. Оно ставится перед функцией, вот так:

async function f() {
  return 1;
}

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

Например, эта функция возвратит выполненный промис с результатом 1:

async function f() {
  return 1;
}

f().then(alert); // 1

Можно и явно вернуть промис, результат будет одинаковым:

async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1

Так что ключевое слово async перед функцией гарантирует, что эта функция в любом случае вернёт промис. Согласитесь, достаточно просто? Но это ещё не всё. Есть другое ключевое слово — await, которое можно использовать только внутри async-функций.

Await

Синтаксис:

// работает только внутри async–функций
let value = await promise;

Ключевое слово await заставит интерпретатор JavaScript ждать до тех пор, пока промис справа от await не выполнится. После чего оно вернёт его результат, и выполнение кода продолжится.

В этом примере промис успешно выполнится через 1 секунду:

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("готово!"), 1000)
  });

*!*
  let result = await promise; // будет ждать, пока промис не выполнится (*)
*/!*

  alert(result); // "готово!"
}

f();

В данном примере выполнение функции остановится на строке (*) до тех пор, пока промис не выполнится. Это произойдёт через секунду после запуска функции. После чего в переменную result будет записан результат выполнения промиса, и браузер отобразит alert-окно «готово!».

Обратите внимание, хотя await и заставляет JavaScript дожидаться выполнения промиса, это не отнимает ресурсов процессора. Пока промис не выполнится, JS-движок может заниматься другими задачами: выполнять прочие скрипты, обрабатывать события и т.п.

По сути, это просто «синтаксический сахар» для получения результата промиса, более наглядный, чем promise.then.

««warn header=»await нельзя использовать в обычных функциях»
Если мы попробуем использовать `await` внутри функции, объявленной без `async`, получим синтаксическую ошибку:

function f() {
  let promise = Promise.resolve(1);
*!*
  let result = await promise; // SyntaxError
*/!*
}

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


Давайте перепишем пример `showAvatar()` из раздела <info:promise-chaining> с помощью `async/await`:

1. Нам нужно заменить вызовы `.then` на `await`.
2. И добавить ключевое слово `async` перед объявлением функции.

```js run
async function showAvatar() {

  // запрашиваем JSON с данными пользователя
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();

  // запрашиваем информацию об этом пользователе из github
  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
  let githubUser = await githubResponse.json();

  // отображаем аватар пользователя
  let img = document.createElement('img');
  img.src = githubUser.avatar_url;
  img.className = "promise-avatar-example";
  document.body.append(img);

  // ждём 3 секунды и затем скрываем аватар
  await new Promise((resolve, reject) => setTimeout(resolve, 3000));

  img.remove();

  return githubUser;
}

showAvatar();
```

Получилось очень просто и читаемо, правда? Гораздо лучше, чем раньше.

````smart header="`await` нельзя использовать на верхнем уровне вложенности"
Программисты, узнав об `await`, часто пытаются использовать эту возможность на верхнем уровне вложенности (вне тела функции). Но из-за того, что `await` работает только внутри `async`–функций, так сделать не получится:

```js run
// SyntaxError на верхнем уровне вложенности
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
```

Можно обернуть этот код в анонимную `async`–функцию, тогда всё заработает:

```js
(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  ...
})();
```


««smart header=»await работает с «thenable»–объектами»
Как и `promise.then`, `await` позволяет работать с промис–совместимыми объектами. Идея в том, что если у объекта можно вызвать метод `then`, этого достаточно, чтобы использовать его с `await`.

В примере ниже, экземпляры класса Thenable будут работать вместе с await:

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // выполнить resolve со значением this.num * 2 через 1000мс
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
};

async function f() {
  // код будет ждать 1 секунду,
  // после чего значение result станет равным 2
  let result = await new Thenable(1);
  alert(result);
}

f();

Когда await получает объект с .then, не являющийся промисом, JavaScript автоматически запускает этот метод, передавая ему аргументы – встроенные функции resolve и reject. Затем await приостановит дальнейшее выполнение кода, пока любая из этих функций не будет вызвана (в примере это строка (*)). После чего выполнение кода продолжится с результатом resolve или reject соответственно.


````smart header="Асинхронные методы классов"
Для объявления асинхронного метода достаточно написать `async` перед именем:

```js run
class Waiter {
*!*
  async wait() {
*/!*
    return await Promise.resolve(1);
  }
}

new Waiter()
  .wait()
  .then(alert); // 1
```
Как и в случае с асинхронными функциями, такой метод гарантированно возвращает промис, и в его теле можно использовать `await`.

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

Когда промис завершается успешно, await promise возвращает результат. Когда завершается с ошибкой – будет выброшено исключение. Как если бы на этом месте находилось выражение throw.

Такой код:

async function f() {
*!*
  await Promise.reject(new Error("Упс!"));
*/!*
}

Делает то же самое, что и такой:

async function f() {
*!*
  throw new Error("Упс!");
*/!*
}

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

Такие ошибки можно ловить, используя try..catch, как с обычным throw:

async function f() {

  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
*!*
    alert(err); // TypeError: failed to fetch
*/!*
  }
}

f();

В случае ошибки выполнение try прерывается и управление прыгает в начало блока catch. Блоком try можно обернуть несколько строк:

async function f() {

  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // перехватит любую ошибку в блоке try: и в fetch, и в response.json
    alert(err);
  }
}

f();

Если у нас нет try..catch, асинхронная функция будет возвращать завершившийся с ошибкой промис (в состоянии rejected). В этом случае мы можем использовать метод .catch промиса, чтобы обработать ошибку:

async function f() {
  let response = await fetch('http://no-such-url');
}

// f() вернёт промис в состоянии rejected
*!*
f().catch(alert); // TypeError: failed to fetch // (*)
*/!*

Если забыть добавить .catch, то будет сгенерирована ошибка «Uncaught promise error» и информация об этом будет выведена в консоль. Такие ошибки можно поймать глобальным обработчиком, о чём подробно написано в разделе info:promise-error-handling.

«`smart header=»async/await и `promise.then/catch`»
При работе с `async/await`, `.then` используется нечасто, так как `await` автоматически ожидает завершения выполнения промиса. В этом случае обычно (но не всегда) гораздо удобнее перехватывать ошибки, используя `try..catch`, нежели чем `.catch`.

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

Так сделано в строке (*) в примере выше.


````smart header="`async/await` отлично работает с `Promise.all`"
Когда необходимо подождать несколько промисов одновременно, можно обернуть их в `Promise.all`, и затем `await`:

```js
// await будет ждать массив с результатами выполнения всех промисов
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
]);

В случае ошибки она будет передаваться как обычно: от завершившегося с ошибкой промиса к Promise.all. А после будет сгенерировано исключение, которое можно отловить, обернув выражение в try..catch.


## Итого

Ключевое слово `async` перед объявлением функции:

1. Обязывает её всегда возвращать промис.
2. Позволяет использовать `await` в теле этой функции.

Ключевое слово `await` перед промисом заставит JavaScript дождаться его выполнения, после чего:

1. Если промис завершается с ошибкой, будет сгенерировано исключение, как если бы на этом месте находилось `throw`.
2. Иначе вернётся результат промиса.

Вместе они предоставляют отличный каркас для написания асинхронного кода. Такой код легко и писать, и читать.

Хотя при работе с `async/await` можно обходиться без `promise.then/catch`, иногда всё-таки приходится использовать эти методы (на верхнем уровне вложенности, например). Также `await` отлично работает в сочетании с `Promise.all`, если необходимо выполнить несколько задач параллельно.

Cover image for Error handling with async/await and promises

Carl Vitullo

Carl Vitullo

Posted on Jul 21, 2018

• Updated on Aug 23, 2018

• Originally published at Medium



 



 



 



 



 

(Photo by Hunter Newton on Unsplash)

I love promises. They’re a fantastic model for asynchronous behavior, and await makes it very easy to avoid callback hell (though I’d argue promises do a great job of that on their own). Once you can build a mental model for how promises work, you can build some very complex asynchronous flows in a handful of lines of code.

As much as I love having async/await in my toolbox, there are several quirks to handling errors when using it. It’s very easy to write error handling in a way that it swallows more errors than you want, and strategies to work around that negate some of the readability advantages that async/await brings.

With async/await, a common way to handle errors when awaiting a promise is to wrap it with a try/catch block. This leads to a relatively straightforward failure case: if you do anything else inside your try block, any exceptions thrown will be caught.

Regular async/await

async () => {
  try {
    const data = await fetchData();
    doSomethingComplex(data);
  } catch (e) {
    // Any errors thrown by `fetchData` or `doSomethingComplex` are caught.
  }
}

This is an unfortunate interaction between async/await and JS exceptions. If JS had a mechanism to catch only certain exceptions, we would be able to describe the errors we want to handle with more precision. Of course, then we’d be writing Java.

The most obvious solution to this is moving your heavy lifting outside of the try block, but this isn’t very satisfying. The flow of data becomes odd, and you can’t use const even though there’s only 1 assignment.

Logic extracted from try blocks

async () => {
  let data;
  try {
    data = await fetchData();
  } catch (e) {
    // Only errors from `fetchData` are caught.
    return;
  }
  doSomethingComplex(data);
};

This code is not particularly pleasant to read and only gets more unpleasant as you handle more potential edge cases. It also requires discipline to keep up and has a high potential for accidentally swallowing errors in the future. Code that requires discipline to maintain correctly is problematic; human error becomes unavoidable beyond a certain scale.

Awaiting a promise doesn’t make it go away, however. Because there is still a promise, you can handle errors as you would without awaiting it.

Await with .catch()

async () => {
  const data = await fetchData().catch(e => {
    // Only errors from `fetchData` are caught.
  });
  if (!data) return;
  doSomethingComplex(data);
};

This works pretty well, as most of the time error handling is relatively self-contained. Your success case still benefits from await without the error handling forcing weird code structure, but it requires you to add a null check on your data. For more complex async flows, I think this will be easier to read and more intuitive to write. Null checks are easy to forget and may introduce bugs that are easy to miss when writing complex flows.

Because of difficulties handling errors without introducing bugs, I prefer to avoid using async/await on anything that’s going to run in the browser. It’s an excellent convenience when I don’t care about failure cases, but programming is difficult, and programming when errors are swallowed is even harder. There are too many pitfalls to put await into wide use.

What about promises?

When dealing with promises without async/await, the choice for error handling is more straightforward. There are only 2 choices: .catch(), or the second argument to .then(). They have one major difference, which I made a demo for a few weeks ago.

Promises with .catch()

() => {
  fetchData()
    .then(data => {
      doSomethingComplex(data);
    })
    .catch(err => {
      // Errors from `fetchData` and `doSomethingComplex` end up here.
    });
};

This has the same problem as our first try/catch block–it handles errors overzealously. Eventually, when I make a typo while editing doSomethingComplex, I’ll lose time because I don’t see the error. Instead, I prefer to use the error argument to .then().

  fetchData()
    .then(
      data => {
        doSomethingComplex(data);
      },
      err => {
        // Only errors from `fetchData` are caught.
      }
    );
};

I rarely use .catch(). I want errors from within my success case to propagate up to where I can see them. Otherwise, any problems during development will be swallowed, increasing the odds that I ship a bug without realizing it.

However, I prefer to handle errors very precisely. I prefer to have bugs surface so that they can be observed and fixed. Stopping errors from propagating may be desirable, if you want the UI to keep chugging through any problems it encounters. Be aware, doing so means only serious failures will be logged.

Other problems with promises

A significant «gotcha» that I’ve run into with promises is that thrown errors within a promise will always cause a rejection. This can be a problem if you’re developing an abstraction over some kind of external data. If you assume that your promise rejection handler only has to handle network errors, you’ll end up introducing bugs. Non-network exceptions won’t make it to your bug tracking tools or will lose important context by the time they do.

const fetchData = () =>
  requestData().then(({ data }) =>
    // What if `removeUnusedFields` throws?
    // It could reference a field on `undefined`, for example.
    data.map(removeUnusedFields)
  );

//
fetchData().then(handleSuccess, err => {
  // This code path is called!
});

This is just how promises behave, but it’s bitten me a few times during development. There isn’t an easy fix for it, so it’s just a case to keep in mind during development. It’s not likely to occur spontaneously in production, but it can cost you time when you’re editing code.

There are always some unknowns when you’re writing code, so it’s safe to assume your error handling will eventually be run with something that it isn’t designed to handle. Imprecise error handling has significant costs in productivity and number of bugs shipped. I encountered an example recently when editing a complex series of asynchronous tasks that used await with try/catch. It threw in the last function call in the try, executing both the success and failure code paths. It took me a while to notice the behavior, and longer to understand why it was happening.

Overall, there are a number of ways that promises can put you into a bad position to handle errors. Understanding how errors will or won’t propagate will help you write code that tolerates faults better. It’s a fine line to tread between handling errors properly and avoiding overly defensive code but it’s one that will pay dividends in the long run.

Looking forward, there is a proposal to add pattern matching (it’s stage 1 at time of writing) that would provide a powerful tool for precisely handling errors. Given the varied ways of describing errors used in different parts of the JS ecosystem, pattern matching looks to be an excellent way of describing them.

For more reading about promises, I recommend this post by Nolan Lawson that was sent to me in response to an earlier draft of this post. Interestingly, he suggests avoiding handling errors in .then(), favoring .catch(), and it’s good to read different perspectives. It talks much more about composing promises together, something I didn’t touch on at all.


Thanks for reading! I’m on Twitter as @cvitullo (but most other places I’m vcarl). I moderate Reactiflux, a chatroom for React developers and Nodeiflux, a chatroom for Node.JS developers. If you have any questions or suggestions, reach out!

Понравилась статья? Поделить с друзьями:
  • Обработка ошибок mysql в php
  • Обработка ошибок ввода чисел c
  • Обработка ошибок can на stm32
  • Обработка ошибок ввода вывода паскаль
  • Обработка ошибки в свойстве ошибка access