Async await js обработка ошибок

Время на прочтение
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? Если да — просим рассказать о том, как вы обрабатываете ошибки в асинхронном коде.

Введение

Совершенно внезапно для себя около года назад я начал писать на 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 по-настоящему или нет.

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

55.3k85 gold badges237 silver badges427 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

90.5k31 gold badges153 silver badges217 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 badges50 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

RolazarRolazar

1101 silver badge8 bronze badges

В этой статье разберём как работает async/await и каким образом его использовать.

Async/await — это специальный синтаксис, который предназначен для более простого и удобного написания асинхронного кода. Появился он в языке, начиная с ES2017 (ES8).

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

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

Асинхронные функции — это такие, которые объявлены с использованием ключевого слова async.

// асинхронная функция delay
async function delay() {
  // ...
}

// асинхронная функция как часть выражения
const wait = async function() {
  // ...
}

// стрелочная запись асинхронной функции sleep
const sleep = async() => {
  // ...
}

В качестве результата асинхронная функция всегда возвращает промис. Если в качестве значения вернуть не промис, то она автоматически обернёт его в успешно завершившийся промис.

const hello = async() => {
  return 'Hello!';
}

const result = hello();

console.log(result); // [object Promise] { ... }

Т.е. результат будет одинаковым, если вместо 'Hello!' вернуть Promise.resolve('Hello!'):

const hello = async() => {
  return Promise.resolve('Hello!');
}

Так как возвращаемое значение является промисом, то мы можем его обработать с помощью метода then():

const hello = async() => {
  return 'Hello!';
}

const result = hello();

result.then(value => console.log(value)); // "Hello!"

…или просто:

const hello = async() => {
  return 'Hello!';
}

hello().then(value => console.log(value)); // "Hello!"

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

const someFunc = async() => {
  throw new Error('Oops');
}

someFunc().catch(error => console.error(error)); // Error: Oops

Внутри асинхронных функций перед промисами можно указывать await.

Await

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

Синтаксис:

// result – переменная, содержащая результат выполнения промиса
let result = await promise;
// на эту строчку не перейдём пока не завершится промис, приведённый выше

Например, выполним последовательно (один после завершения другого) три промиса внутри асинхронной функции:

// функция, возвращающая промис
function delay(ms, str) {
  return new Promise (resolve => setTimeout(() => {
    resolve(str);
  }, ms));
}
// асинхронная функция
async function message() {
  // ждём выполнение первого промиса delay(1000, 'Игорь') и сохраняем его результат в переменную a
  let a = await delay(1000, 'Игорь');
  // после завершения первого промиса, переходим к выполнению второго delay(2000, 'Егор')
  let b = await delay(2000, 'Егор'); // как только он завершится помещаем его результат в переменную b
  // после завершения второго промиса, переходим к выполнению третьего delay(4000, 'Денис')
  let c = await delay(4000, 'Денис'); // как только он выполнится сохраняем его результат в переменную c

  // выводим значения переменных в консоль
  console.log(`${a} ${b} ${c}`); // "Игорь Егор Денис"
}

// вызываем асинхронную функцию
message();

Не смотря на то, что await заставляет интерпретатор остановиться и дождаться завершения промиса, движок JavaScript в это время может выполнять другие задачи. Это достигается благодаря тому, что асинхронный код выполняется в фоне и не блокирует основной поток. Таким образом, await не приводит к «зависанию» страницы и не мешает пользователю взаимодействовать с ней.

По сути, «async/await» – это просто удобный способ работать с промисами. Если переписать код, приведённый выше через промисы (без использования «async/await»), то получится примерно следующее:

// функция, возвращающая промис
function delay(ms, str) {
  return new Promise (resolve => setTimeout(() => {
    resolve(str);
  }, ms));
}
// код, переписанный через промисы
let a, b, c;
delay(1000, 'Игорь')
  .then(value => {
    a = value;
    return delay(2000, 'Егор');
  })
  .then(value => {
    b = value;
    return delay(4000, 'Денис');
  })
  .then(value => {
    c = value;

    console.log(`${a} ${b} ${c}`); // "Игорь Егор Денис"
  })

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

Обработать потенциальные ошибки в асинхронной функции можно с помощью try..catch. Для этого этот блок кода (в котором используется await) необходимо заключить в эту конструкцию.

Например:

async function getUser() {
  try {
    const response = await fetch(url);
    const data = await response.json();
  } catch(e) {
    // если что-то пойдёт не так на каком-то этапе в блоке try, то мы автоматически попадём в метод catch()
    console.error(e);
  }
}

Если нужно с finally, то так:

async function getUser() {
  try {
    const response = await fetch(url);
    const data = await response.json();
  } catch(e) {
    // если что-то пойдёт не так на каком-то этапе в блоке try, то мы автоматически попадём в метод catch()
    console.error(e);
  } finally {
    // выполнится в любом случае, в независимости от того произошла ошибка или нет
  }
}

Пример, в котором напишем асинхронный код для получения данных с сервера JSONPlaceholder:

Асинхронный код для получения данных с сервера JSONPlaceholder

// асинхронная функция для получения данных пользователя в формате JSON
const getUser = async(id) => {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
    if (!response.ok) {
      throw new Error('Ответ сервера не в диапазоне 200-299');
    }
    const data = await response.json();

    return data;
  } catch (e) {
    throw new Error('Ошибка при получении данных пользователя');
  }
}
// функция для отображения данных пользователя на странице
const renderUsers = (users) => { ... }
// асинхронная функция, в которой сначала вызывается getUser(), а затем renderUsers() для отображения полученных на странице
const clickUser = async(id) => {
  try {
    const data = await getUser(id);
    const users = Array.isArray(data) ? data : [data];
    renderUsers(users);
  } catch (e) {
    console.error(e);
  }
}

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.

Понравилась статья? Поделить с друзьями:
  • Asustpapi dll ошибка
  • Aternos ошибка не удалось проверить имя пользователя
  • Asus update ошибка
  • Asyg09llca коды ошибок
  • Aternos ошибка запуска сервера