Давайте рассмотрим этот Promise, который возвращает данные или исключение через 2 секунды в зависимости от параметра rejectPromise:
// api.js
const fetchData = async (duration, rejectPromise) => (
new Promise((resolve, reject) => {
setTimeout(() => {
if (rejectPromise) {
reject({
error: 'Error Encountered',
status: 'error'
})
}
resolve({
version: 1,
hello: 'world',
});
}, duration);
})
);
module.exports = {
fetchData,
};
Типичное использование этой функции будет выглядеть так:
const { fetchData } = require('./api');
const callApi = async () => {
try {
const value = await fetchData(2000, false);
console.info(value);
} catch (error) {
console.error(error);
}
}
callApi();
/*
OUTPUT:
{ version: 1, hello: 'world' } (rejectPromise=false)
{ error: 'Error Encountered', status: 'error' } (rejectPromise=true)
*/
Как вы можете видеть, когда параметр rejectPromise является ложным, Promise возвращает значение {version: 1, hello: 'world'}, а когда оно истинно, он вызывает исключение { error: 'Error Encountered', status: 'error' }.
Это типичная реализация async-await. Теперь мы попытаемся использовать Promise, чтобы сделать этот код более простым. Давайте напишем функцию-обертку, которая упростит нам обработку исключений.
// wrapper.js
const wrapper = promise => (
promise
.then(data => ({ data, error: null }))
.catch(error => ({ error, data: null }))
);
module.exports = wrapper;
</source
Мы можем видеть, что функция-обертка принимает Promise в качестве входного параметра и возвращает ошибку или даные с использовнаием конструкции then().catch(). Итак, давайте изменим исходный код с применением функции-обертки:
<source lang="javascript">
const { fetchData } = require('./api');
const wrapper = require('./wrapper');
const callApi = async () => {
const { error, data } = await wrapper(fetchData(2000, false));
if (!error) {
console.info(data);
return;
}
console.error(error);
}
callApi();
/*
OUTPUT:
{ version: 1, hello: 'world' } (rejectPromise=false)
{ error: 'Error Encountered', status: 'error' } (rejectPromise=true)
*/
— получаем тот же результат но более читабельным кодом.
Полезные ссылки.
1. Почему [не] надо использовать async/await
Комментарии (23)
mwizard
18.05.2018 22:57+3А потом кто-то забывает обработать error, и вот, привет игнорирование исключений в самых неожиданных местах.
И да, толку от обработки вида `console.error(err)` очень мало. Может, вовсе не стоит ловить исключение, если оно обрабатывается таким образом?apapacy Автор
19.05.2018 00:25-2Есть одна неприятная вещь в промисах равно как и в асинках. Если не будет обработан reject то nodejs завершит работу с ошибкой. Это по сути обязывает разработчика все конструкции await заключать в блоки trycatch. Поэтому способ предложенный автором снимает определённую часть проблем.
Daraku
19.05.2018 01:18+2Поэтому способ предложенный автором снимает определённую часть проблем.
Автор утверждает что: «try/catch-джунгли» ничем не лучше «callback-джунглей».
И он прав, но предложенное решение это «if/else-джунгли». В этом плане все перечисленные решения одинаковы, так как являются вариацией одного и того же явления: "Pyramid of doom"apapacy Автор
19.05.2018 13:33Из callback-джунглей был предложен хороший выход github.com/creationix/step. Я так ни разу не воспользовался этой библиотекой т.к все быстро перешли на промисы. Хотя с учётом того что промисы стали наличными и быстрыми не сразу указанное решение могло бы упростить разработку
Alternator
19.05.2018 17:51+1Более популярна для callback-джунглей библиотека www.npmjs.com/package/async
В ней и функциональности больше, да и API на мой взгляд чище.
Кроме того есть ее порт для промисов — www.npmjs.com/package/async-q
Также полезна, так как организация высокоуровневых шаблонов асинхронности(ограничение количества потоков, очереди) в промисах все-таки отсутсвует
lega
19.05.2018 02:17+3обязывает разработчика все конструкции await заключать в блоки trycatch
А нельзя сделать один trycatch где-то наверху, там где реквест/задача начинается?apapacy Автор
19.05.2018 02:50-3Единственный вариант — это в каждой асинхронной функции взять все в try/catch блок. Если это сделать где-то на самом верхнем уровне вызовов функции — то я сталкивался с ситуацией что это не помогает. Нужно будет специально исследовать этот вопрос.
Alternator
19.05.2018 11:46+1Нужен конкретный пример, где один внешний try-catch не работает
Проверил на базовых примерах — внешний try-catch прекрасно работает
Может сломаться, если не await-ить внутренние вызовы, но тут и try-catch-hell не спасет
Как следствие может сломаться на изощренных юзкейсах, вроде «послать все запросы в цикле, их промисы запомним в массив, а потом в однопоточном цикле их await-ить и обрабатывать»
Но в таких нетривиальных случаях пожалуй лучше будет использовать библиотеки управления потоком, или как workaround, вызвать перед цикломawait Promise.race(results)
Как следствие try-catch-и можно размещать согласно логике приложение(там где вы хотите реально обработать ошибку), например на уровне обработчика запроса — точно также, как это делается для синхронного кодаapapacy Автор
19.05.2018 12:15Я когда писал комментарий не помнил точно где это встречалось. Потом проверил. На уровне модуля то есть не внутри функции нельзя использовать конструкцию await. За этот факт и за необходимость явно определять функцию как async есть справедливая критика. То есть вызов на верхнем уровне асинхронного кода в trycatch невозможен. Вернее он не будет ловить ошибку так как нельзя использовать await
Alternator
19.05.2018 12:46В таком случае можно обернуть верхний уровень в авто-вызываемую асинхронную функцию, и обработать ошибку в нем
(async function main() { await func1(); await func2(); })().catch((err) => console.error(err))
Также, вероятно на верхнем уровне асинхронные функции вызываются синхронно, одна за другой, потому что должны идти параллельно и независимо (иначе они уже были бы обернуты в асинхронную функцию, либо цепочку промисов).
Тогда стоит рассматривать эти функции как верхнеуровневые, и достаточно добавить try-catch только в них, либо вообще обработать их ошибки на уровне модуля
func1(); func2(); async function func1() { try { // ... } catch(e) { console.log(e); } } async function func2() { try { // ... } catch(e) { console.log(e); } }
func1().catch(e => console.log(e)); func2().catch(e => console.log(e));; async function func1() { // ... } async function func2() { // ... }
В итоге try-catch все еще не нужен в каждой асинхронной функции, и мы получаем обработку ошибок на верхнем уровне(и в тех функциях где она нужна по логике приложения)
При этом, если исключения доходят до верхнего уровня, то логично использовать uncaughtException/rejectionHandledapapacy Автор
19.05.2018 13:08Да все верно. Единственное что хотелось бы получать более простой для восприятия код. В этом смысле await как кажется на первый взгляд сильно все упрощает. Но когда начинаешь работать с конкретной задачей то сталкиваешься с необходимостью обрабатывать ошибки. С учётом появления стрелочных функций которые упростили код с промисами я бы сказал что по восприятию код с промисами и стрелочными функциями и код с async/await примерно равны по простоте/сложности для восприятия.
Daraku
19.05.2018 00:18+2получаем тот же результат но более читабельным кодом
В статье не хватает определения читабельности.
Примеры недостаточно показывают разницу, так как, положительный пример имеет:
- Одинаковое количество строк кода в теле функции как и «try/catch» пример (6 строк)
- Одинаковое количество условий выхода из функции («return;/console.error(error)» и «try/catch»)
- Отрицательное условие: «если не ошибка» (if (!error))
- Двойную интерпретацию «wrapper(fetchData(2000, false))», в зависимость от понимания работы «fetchData»:
- «fetchData» — синхронна и возвращает данные на обработку в «wrapper»:
const dataRaw = fetchData(2000, false); const data = await wrapper(dataRaw);
- «wrapper» — модифицирует «Promise» объект, возвращаемый вызовом «fetchData»:
const fetchPromise = fetchData(2000, false); const data = await wrapper(fetchPromise);
- «fetchData» — синхронна и возвращает данные на обработку в «wrapper»:
В зависимости от принятых соглашениях о качестве кода, данный пример может быть принят как положительно влияющий на читаемость кода, так и как отрицательно.
marshinov
19.05.2018 11:42+1Обратите внимание на вот такой подход: https://m.habr.com/post/339606/. Все try/catch уйдут, но обработка ошибок останется
F0iL
19.05.2018 11:55+7const { error, data } = await wrapper(fetchData(2000, false));
if (!error) {
Что-то мне это напоминает… :)
Keyten
19.05.2018 15:17Я заодно оставлю здесь более удобный способ создавать промисы.
Кодfunction Defer(){ var status; this.resolve = function(value){ status = { type: 'resolved', value: value }; }; this.reject = function(value){ status = { type: 'rejected', value: value }; }; var that = this; this.promise = function(){ return promise = new Promise((resolve, reject) => { if(status){ if(status.type === 'resolved'){ resolve(status.value); } else { reject(status.value); } } else { that.resolve = function(value){ resolve(value); } that.reject = function(value){ reject(value); } } }); }; }
lega
19.05.2018 15:36Чем он более удобнее?, это стиль «deferred», от которого все* отказались много лет назад и перешли на промисы, в котором меньше кейвордов нужно помнить.
Keyten
19.05.2018 15:45Тем, что не нужно создавать 20 вложенных друг в друга функций.
Лично мне наоборот сложнее, когда функций больше. У них ещё разные контексты исполнения, разные this (arrow functions не везде), где-то его нужно передавать, где-то нет… опять callback hell, в общем.justboris
19.05.2018 16:35+1Откуда вы взяли цифру 20?
new Promise
добавляет только одну дополнительную функцию в код, зато вы получаете важное преимущество — синхронно выброшенное исключение перехватится и зарежектит промис. Вы гарантированно никогда не получите синхронного исключения.Keyten
19.05.2018 19:33Число 20 появилось из традиционной для русского языка гиперболы.
Про исключения не знал. Тогда всё становится понятнее, окей.
lega
19.05.2018 21:50Тем, что не нужно создавать 20 вложенных друг в друга функций.
Это наверное при каком-то неправильном использовании, есть пример? Промисы можно более менее линейно «стыковать».
justboris
19.05.2018 16:47+1На самом деле, для подхода, описанного в статье, необязательно даже wrapper писать. Достаточно резолвить промис с ошибкой вместо режекта.
const fetchData = async (rejectPromise) => new Promise(resolve => { if (rejectPromise) { resolve({ error: "Error Encountered" }); } else { resolve({ data: { version: 1, hello: "world" } }); } }); // использование будет таким же const { error, data } = await fetchData(true);
Разумеется, это будет работать только для вашего кода, для библиотек нужна обертка.
rumkin
20.05.2018 07:01Согласен, некоторые разработчики отрицательный результат вызова функции возвращают через исключение (особенно часто это встречается в валидаторах), что бывает очень неудобно. Но лучше сделать обертки для этих функций до их использования, добавив обработку нужных исключений, нежели оборачивать каждый вызов.
mak_ufo
Так и не понял. Зачем функция из первого примера помечена как async, если внутри нигде не вызывается await?