Pull to refresh

Comments 24

Так и не понял. Зачем функция из первого примера помечена как async, если внутри нигде не вызывается await?
А потом кто-то забывает обработать error, и вот, привет игнорирование исключений в самых неожиданных местах.

И да, толку от обработки вида `console.error(err)` очень мало. Может, вовсе не стоит ловить исключение, если оно обрабатывается таким образом?
Есть одна неприятная вещь в промисах равно как и в асинках. Если не будет обработан reject то nodejs завершит работу с ошибкой. Это по сути обязывает разработчика все конструкции await заключать в блоки trycatch. Поэтому способ предложенный автором снимает определённую часть проблем.
Поэтому способ предложенный автором снимает определённую часть проблем.

Автор утверждает что: «try/catch-джунгли» ничем не лучше «callback-джунглей».

И он прав, но предложенное решение это «if/else-джунгли». В этом плане все перечисленные решения одинаковы, так как являются вариацией одного и того же явления: "Pyramid of doom"
Из callback-джунглей был предложен хороший выход github.com/creationix/step. Я так ни разу не воспользовался этой библиотекой т.к все быстро перешли на промисы. Хотя с учётом того что промисы стали наличными и быстрыми не сразу указанное решение могло бы упростить разработку
Более популярна для callback-джунглей библиотека www.npmjs.com/package/async
В ней и функциональности больше, да и API на мой взгляд чище.

Кроме того есть ее порт для промисов — www.npmjs.com/package/async-q
Также полезна, так как организация высокоуровневых шаблонов асинхронности(ограничение количества потоков, очереди) в промисах все-таки отсутсвует
обязывает разработчика все конструкции await заключать в блоки trycatch
А нельзя сделать один trycatch где-то наверху, там где реквест/задача начинается?
Единственный вариант — это в каждой асинхронной функции взять все в try/catch блок. Если это сделать где-то на самом верхнем уровне вызовов функции — то я сталкивался с ситуацией что это не помогает. Нужно будет специально исследовать этот вопрос.
Нужен конкретный пример, где один внешний try-catch не работает
Проверил на базовых примерах — внешний try-catch прекрасно работает

Может сломаться, если не await-ить внутренние вызовы, но тут и try-catch-hell не спасет
Как следствие может сломаться на изощренных юзкейсах, вроде «послать все запросы в цикле, их промисы запомним в массив, а потом в однопоточном цикле их await-ить и обрабатывать»
Но в таких нетривиальных случаях пожалуй лучше будет использовать библиотеки управления потоком, или как workaround, вызвать перед циклом
await Promise.race(results)


Как следствие try-catch-и можно размещать согласно логике приложение(там где вы хотите реально обработать ошибку), например на уровне обработчика запроса — точно также, как это делается для синхронного кода
Я когда писал комментарий не помнил точно где это встречалось. Потом проверил. На уровне модуля то есть не внутри функции нельзя использовать конструкцию await. За этот факт и за необходимость явно определять функцию как async есть справедливая критика. То есть вызов на верхнем уровне асинхронного кода в trycatch невозможен. Вернее он не будет ловить ошибку так как нельзя использовать await
В таком случае можно обернуть верхний уровень в авто-вызываемую асинхронную функцию, и обработать ошибку в нем
(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/rejectionHandled
Да все верно. Единственное что хотелось бы получать более простой для восприятия код. В этом смысле await как кажется на первый взгляд сильно все упрощает. Но когда начинаешь работать с конкретной задачей то сталкиваешься с необходимостью обрабатывать ошибки. С учётом появления стрелочных функций которые упростили код с промисами я бы сказал что по восприятию код с промисами и стрелочными функциями и код с async/await примерно равны по простоте/сложности для восприятия.
получаем тот же результат но более читабельным кодом

В статье не хватает определения читабельности.

Примеры недостаточно показывают разницу, так как, положительный пример имеет:
  • Одинаковое количество строк кода в теле функции как и «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);


В зависимости от принятых соглашениях о качестве кода, данный пример может быть принят как положительно влияющий на читаемость кода, так и как отрицательно.
const { error, data } = await wrapper(fetchData(2000, false));
if (!error) {

Что-то мне это напоминает… :)
image
Я заодно оставлю здесь более удобный способ создавать промисы.
Код
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);
                }
            }
        });
    };
}


С ним можно делать более читаемый код:
const fetchData = (duration, rejectPromise) => {
    var defer = new Defer();

    setTimeout(() => {
        if(rejectPromise){
            defer.reject({
                error: 'Error Encountered',
                status: 'error'
            });
        } else {
            defer.resolve({
                version: 1,
                hello: 'world',
            });
        }
    }, duration);

    return defer.promise();
}
Чем он более удобнее?, это стиль «deferred», от которого все* отказались много лет назад и перешли на промисы, в котором меньше кейвордов нужно помнить.
Тем, что не нужно создавать 20 вложенных друг в друга функций.
Лично мне наоборот сложнее, когда функций больше. У них ещё разные контексты исполнения, разные this (arrow functions не везде), где-то его нужно передавать, где-то нет… опять callback hell, в общем.

Откуда вы взяли цифру 20? new Promise добавляет только одну дополнительную функцию в код, зато вы получаете важное преимущество — синхронно выброшенное исключение перехватится и зарежектит промис. Вы гарантированно никогда не получите синхронного исключения.

Число 20 появилось из традиционной для русского языка гиперболы.

Про исключения не знал. Тогда всё становится понятнее, окей.
Тем, что не нужно создавать 20 вложенных друг в друга функций.
Это наверное при каком-то неправильном использовании, есть пример? Промисы можно более менее линейно «стыковать».

На самом деле, для подхода, описанного в статье, необязательно даже 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);

Разумеется, это будет работать только для вашего кода, для библиотек нужна обертка.

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

Only those users with full accounts are able to leave comments. Log in, please.