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

127

Почему я не могу просто вызвать 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проглатывает черная дыра?

demian85
источник
11
Его не проглатывает черная дыра. Он отвергает обещание, которое .catch(…)возвращается.
Берги
вместо .catch((e) => { throw new Error() }), напиши .catch((e) => { return Promise.reject(new Error()) })или просто.catch((e) => Promise.reject(new Error()))
chharvey
1
@chharvey: все фрагменты кода в вашем комментарии ведут себя точно так же, за исключением того, что начальный вариант наиболее понятен.
Сергей Гринько

Ответы:

157

Как объясняли другие, «черная дыра» возникает из-за того, что бросание внутрь .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!
});

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

выходка

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

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

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

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

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

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

кливер
источник
3
Джиб, это интересный трюк, не могли бы вы помочь мне понять, почему это работает?
Брайан Кейт,
8
Об этой уловке: вы бросаете, потому что хотите войти, так почему бы просто не войти напрямую? Этот трюк в «случайное» время вызовет непреодолимую ошибку ... Но вся идея исключений (и способ, которым промисы справляются с ними) состоит в том, чтобы сделать ответственность вызывающей стороны за обнаружение ошибки и устранение ее. Этот код фактически делает невозможным для вызывающей стороны устранение ошибок. Почему бы просто не создать функцию, которая справится с этим за вас? function logErrors(e){console.error(e)}тогда используйте это как do1().then(do2).catch(logErrors). Ответ сам по себе отличный, кстати, +1
Stijn de Witt
3
@jib Я пишу лямбду AWS, которая содержит много обещаний, связанных более или менее, как в этом случае. Чтобы использовать сигналы тревоги и уведомления AWS в случае ошибок, мне нужно сделать так, чтобы лямбда-сбой выдавала ошибку (я думаю). Уловка - единственный способ получить это?
masciugo
2
@StijndeWitt В моем случае я пытался отправить сведения об ошибке на свой сервер в window.onerrorобработчике событий. setTimeoutЭто можно сделать, только выполнив фокус. В противном случае window.onerrorникогда не услышите ничего об ошибках, произошедших в Promise.
hudidit
1
@hudidit Тем не менее, будь то console.logили postErrorToServer, ты можешь просто делать то, что нужно. Нет причин, по которым любой код не window.onerrorможет быть выделен в отдельную функцию и вызван из двух мест. Наверное, даже короче setTimeoutочереди.
Stijn de Witt
46

Важно понять здесь

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

  2. Либо бросок, либо явный отказ переместит текущее обещание в отклоненное состояние.

  3. Поскольку thenи catchвозвращают новые объекты обещаний, их можно связывать в цепочку.

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

  5. Как отметил 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обработчике.

thefourtheye
источник
3
Также стоит добавить, что .then()обработчики являются асинхронными (стек разматывается перед их выполнением), поэтому исключения внутри них должны быть преобразованы в отклонения, иначе не было бы обработчиков исключений, которые их перехватили.
jfriend00 08
7

Я пробовал 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;
});
RiggerTheGeek
источник
3
У Jest есть имитация таймера, которые должны справиться с этой ситуацией.
jordanbtucker
2

Согласно спецификации (см. 3.III.d) :

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

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

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

просто-борис
источник
Стоит отметить, что unhandledRejectionловушка предназначена для серверного JavaScript, на стороне клиента разные браузеры имеют разные решения. МЫ еще не стандартизировали это, но постепенно, но верно.
Бенджамин Грюнбаум
1

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

Я добавил небольшую вспомогательную функцию, которая возвращает обещание, например:

function throw_promise_error (error) {
 return new Promise(function (resolve, reject){
  reject(error)
 })
}

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

}).then(function (input) {
 if (input === null) {
  let err = {code: 400, reason: 'input provided is null'}
  return throw_promise_error(err)
 } else {
  return noterrorpromise...
 }
}).then(...).catch(function (error) {
 res.status(error.code).send(error.reason);
})

Таким образом, я контролирую выброс дополнительных ошибок из цепочки обещаний. Если вы хотите также обрабатывать «нормальные» ошибки обещаний, вы должны расширить свой улов, чтобы обрабатывать «самовыданные» ошибки отдельно.

Надеюсь, это поможет, это мой первый ответ на stackoverflow!

nuudles
источник
Promise.reject(error)вместо new Promise(function (resolve, reject){ reject(error) })(для чего в любом случае потребуется оператор return)
Funkodebat
0

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

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});
Хесус Каррера
источник
1
Нет, этого недостаточно, так как вам нужно будет поместить это в конец каждой цепочки обещаний. Скорее зацепило unhandledRejectionсобытие
Берги
Да, это при условии, что вы связываете свои обещания так, чтобы выход был последней функцией, и она не отслеживалась после. Событие, о котором вы упомянули, я думаю, это только при использовании Bluebird.
Хесус Каррера
Bluebird, Q, когда, родные обещания,… Скорее всего, он станет стандартом.
Берги