Как правильно обрабатывать ошибки в цепочке Promise?

Скажем, у нас есть 3 асинхронных задачи, которые возвращают Promises: A, B и C. Мы хотим связать их вместе (т.е. Для ясности, принимая значение, возвращаемое A и вызывая B с ним), но также хотим правильно обрабатывать ошибки для каждого и выходить из строя при первом сбое, В настоящее время я вижу два способа сделать это:

A
.then(passA)
.then(B)
.then(passB)
.then(C)
.then(passC)
.catch(failAll)

Здесь функции passX обрабатывают каждый успешный вызов X. Но в функции failAll нам пришлось бы обрабатывать все ошибки A, B и C, которые могут быть сложными и непростыми для чтения, особенно если у нас было более 3 асинхронных задач, Таким образом, другой подход учитывает это:

A
.then(passA, failA)
.then(B)
.then(passB, failB)
.then(C)
.then(passC, failC)
.catch(failAll)

Здесь мы выделили логику исходного failAll в failA, failB и failC, что кажется простым и читаемым, поскольку все ошибки обрабатываются прямо рядом с его источником. Однако это не делает то, что я хочу.

Посмотрим, не сработает ли A (отклонено), failA не должен переходить на вызов B, поэтому он должен выбросить исключение или отклонить вызов. Но оба они попадают под failB и failC, что означает, что failB и failC должны знать, были ли мы уже сработали или нет, предположительно, сохраняя состояние (т.е. переменную).

Более того, кажется, что чем больше задач async у нас есть, тем больше наша функция failAll растет (путь 1), или больше функций failX вызывается (путь 2). Это подводит меня к моему вопросу:

Есть ли лучший способ сделать это?

Рассмотрение. Поскольку исключения из then обрабатываются методом отклонения, должен ли существо быть метод Promise.throw, чтобы фактически отключить цепочку?

A возможно дублировать, с ответом, который добавляет больше областей внутри обработчиков. Разве promises не выполняет функции линейной цепочки функций, а не передает функции, которые передают функции, которые передают функции?

Ответ 1

У вас есть пара вариантов. Во-первых, давайте посмотрим, смогу ли я отменить ваши требования.

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

  • Когда одно обещание терпит неудачу, вы хотите иметь возможность прервать остальную цепочку.

Одна из возможностей такова:

A().then(passA).catch(failA).then(val => {
    return B(val).then(passB).catch(failB);
}).then(val => {
    return C(val).then(passC).catch(failC);
}).then(finalVal => {
    // chain done successfully here
}).catch(err => {
    // some error aborted the chain, may or may not need handling here
    // as error may have already been handled by earlier catch
});

Затем в каждом failA, failB, failC вы получите конкретную ошибку для этого шага. Если вы хотите прервать цепочку, вы снова свернете, прежде чем функция вернется. Если вы хотите, чтобы цепочка продолжалась, вы просто возвращаете нормальное значение.


Вышеприведенный код также можно записать следующим образом (при немного отличающемся поведении, если passB или passC выбрасывает или возвращает отклоненное обещание.

A().then(passA, failA).then(val => {
    return B(val).then(passB, failB);
}).then(val => {
    return C(val).then(passC, failC);
}).then(finalVal => {
    // chain done successfully here
}).catch(err => {
    // some error aborted the chain, may or may not need handling here
    // as error may have already been handled by earlier catch
});

Так как они полностью повторяются, вы можете заставить все это руководствоваться таблицей для любой длины последовательности.

function runSequence(data) {
    return data.reduce((p, item) => {
        return p.then(item[0]).then(item[1]).catch(item[2]);
    }, Promise.resolve());
}

let fns = [
    [A, passA, failA],
    [B, passB, failB],
    [C, passC, failC]
];

runSequence(fns).then(finalVal => {
    // whole sequence finished
}).catch(err => {
    // sequence aborted with an error
});

Еще один полезный момент при объединении множества promises - если вы создаете уникальный класс ошибок для каждой ошибки отклонения, тогда вы можете более легко включить тип ошибки с помощью instanceof в последнем обработчике .catch(), если вы нужно знать, какой шаг вызвал прерванную цепь. Библиотеки, такие как Bluebird, предоставляют определенную семантику .catch() для создания .catch(), которая улавливает только определенный тип ошибки (например, способ try/catch делает это). Вы можете видеть, как Bluebird делает это здесь: http://bluebirdjs.com/docs/api/catch.html. Если вы собираетесь обрабатывать каждую ошибку прямо в своем собственном отказе от обещания (как в приведенных выше примерах), тогда это не требуется, если вам еще не нужно знать на последнем этапе .catch(), на котором шаг вызвал ошибку.

Ответ 2

Есть два способа, которые я рекомендую (в зависимости от того, что вы пытаетесь сделать с этим):

Да, вы хотите обрабатывать все ошибки в цепочке обещаний с помощью одного catch.

Если вам нужно знать, какой из них не удалось, вы можете отклонить обещание с помощью уникального сообщения или значения, подобного этому:

A
.then(a => {
  if(!pass) return Promise.reject('A failed');
  ...
})
.then(b => {
  if(!pass) return Promise.reject('B failed');
  ...
})
.catch(err => {
  // handle the error
});

В качестве альтернативы вы можете вернуть другой promises внутри .then

A
.then(a => {
  return B; // B is a different promise
})
.then(b => {
  return C; // C is another promise
})
.then(c => {
  // all promises were resolved
  console.log("Success!") 
})
.catch(err => {
  // handle the error
  handleError(err)
});

В каждом из этих promises вам понадобится какое-то уникальное сообщение об ошибке, чтобы вы знали, какой из них не удалось.

И поскольку это функции стрелок, мы можем удалить фигурные скобки! Еще одна причина, по которой я люблю promises

A
.then(a => B)
.then(b => C)
.then(c => console.log("Success!"))
.catch(err => handleError(err));

Ответ 3

Вы можете разделить цепочку обещаний, но, честно говоря, ранняя обработка ошибок - это не правильный способ сделать это, особенно по таким грубым причинам, как читаемость. Истинно для синхронного кода, т.е. Не try/catch для каждой отдельной функции, или читаемость просто идет в корзину.

Всегда пропускайте ошибки и "обрабатывайте" их в точке, где возобновляется поток положительного кода.

Если вам нужно знать, как далеко все прошло, трюк, который я использую, - простой счетчик прогрессии:

let progress = "";
A()
.then(a => (progress = "A passed", passA(a)))
.then(B)
.then(b => (progress = "B passed", passB(b)))
.then(C)
.then(c => (progress = "C passed", passC(c)))
.catch(err => (console.log(progress), failAll(err)))