Почему я не могу бросить внутри обработчика Promise.catch?

Почему я не могу просто выбросить Error внутри обратного вызова catch и позволить процессу обрабатывать ошибку, как если бы она находилась в любой другой области?

Если я не делаю console.log(err), ничего не распечатывается, и я ничего не знаю о том, что произошло. Процесс заканчивается...

Пример:

function do1() {
    return new Promise(function(resolve, reject) {
        throw new Error('do1');
        setTimeout(resolve, 1000)
    });
}

function do2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error('do2'));
        }, 1000)
    });
}

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // This does nothing
});

Если обратные вызовы выполняются в основном потоке, почему Error проглатывается черной дырой?

Ответ 1

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

Добавьте еще один улов, чтобы увидеть, что происходит:

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // Where does this go?
}).catch(function(err) {
    console.log(err.stack); // It goes here!
});

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

Trick

Чтобы сообщение об ошибке появилось как ошибка в веб-консоли, как вы изначально предполагали, я использую этот трюк:

.catch(function(err) { setTimeout(function() { throw err; }); });

Даже номера строк сохраняются, поэтому ссылка в веб-консоли выводит меня прямо в файл и строку, где произошла ошибка (оригинал).

Почему это работает

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

Функция, вызываемая setTimeout, с другой стороны, всегда работает из состояния стабилизации JavaScript, то есть она запускается в новом цикле в цикле событий JavaScript. Исключения там не пойманы ничем и не попадают в веб-консоль. Поскольку err содержит всю информацию об ошибке, включая исходный стек, файл и номер строки, он по-прежнему сообщается правильно.

Ответ 2

Важные вещи, чтобы понять здесь

  • Обе функции then и catch возвращают новые объекты обещания.

  • Либо бросая, либо явно отклоняя, переносит текущее обещание на отклоненное состояние.

  • Так как then и catch возвращают новые объекты обещания, они могут быть скованы.

  • Если вы бросите или отклоните внутри обработчика обещаний (then или catch), он будет обработан в следующем обработчике отклонения по цепочке цепочек.

  • Как упоминалось jfriend00, обработчики then и catch не выполняются синхронно. Когда обработчик бросает, он немедленно прекратится. Таким образом, стек будет размотан, и исключение будет потеряно. Вот почему бросание исключения отвергает существующее обещание.


В вашем случае вы отклоняете внутри do1, бросая объект Error. Теперь текущее обещание будет в отклоненном состоянии, и управление будет передано следующему обработчику, который then в нашем случае.

Так как обработчик then не имеет обработчика отклонения, do2 не будет выполняться вообще. Вы можете подтвердить это, используя console.log внутри него. Поскольку текущее обещание не имеет обработчика отклонения, оно также будет отклонено с использованием значения отклонения от предыдущего обещания, и элемент управления будет передан следующему обработчику, который равен catch.

Поскольку catch является обработчиком отклонения, когда вы выполняете console.log(err.stack); внутри него, вы можете видеть трассировку стека ошибок. Теперь вы бросаете из него объект Error, так что обещание, возвращаемое catch, также будет находиться в состоянии отклонения.

Поскольку вы не привязали обработчик отклонения к catch, вы не можете наблюдать отклонение.


Вы можете разделить цепочку и понять это лучше, например

var promise = do1().then(do2);

var promise1 = promise.catch(function (err) {
    console.log("Promise", promise);
    throw err;
});

promise1.catch(function (err) {
    console.log("Promise1", promise1);
});

Результат, который вы получите, будет выглядеть как

Promise Promise { <rejected> [Error: do1] }
Promise1 Promise { <rejected> [Error: do1] }

Внутри обработчика catch 1 вы получаете значение promise объекта как отклоненного.

То же самое, обещание, возвращенное обработчиком catch 1, также отклоняется с той же ошибкой, с которой был отклонен promise, и мы наблюдаем его во втором обработчике catch.

Ответ 3

В соответствии с спецификацией (см. 3.III.d):

д. Если вызов затем выдает исключение e,
  а. Если вы вызываете resolvePromise или rejectPromise, игнорируйте его.
  б. В противном случае, отклоните обещание с e в качестве причины.

Это означает, что если вы выбрали исключение в функции then, оно будет поймано, и ваше обещание будет отклонено. catch здесь не имеет смысла, это просто сокращение от .then(null, function() {})

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

Ответ 4

Я попробовал метод setTimeout(), описанный выше...

.catch(function(err) { setTimeout(function() { throw err; }); });

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

Я вернулся к простому прослушивателю, который отлично работал и, поскольку он предназначен для использования JavaScript, был высоко проверяемым.

return new Promise((resolve, reject) => {
    reject("err");
}).catch(err => {
    this.emit("uncaughtException", err);

    /* Throw so the promise is still rejected for testing */
    throw err;
});

Ответ 5

Да promises ошибки проглатывания, и вы можете их поймать только с помощью .catch, как объяснено более подробно в других ответах. Если вы находитесь в Node.js и хотите воспроизвести обычное поведение throw, выполните трассировку стека стека на консоль и завершите процесс, вы можете сделать

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});